From 1e4c852380635dfa6cca4427f423ecce7899a19f Mon Sep 17 00:00:00 2001 From: Felix Yuan Date: Wed, 28 Jun 2023 10:46:12 -0700 Subject: [PATCH 1/4] Long running transactions collector and test Signed-off-by: Felix Yuan --- collector/pg_long_running_transactions.go | 93 +++++++++++++++++++ .../pg_long_running_transactions_test.go | 63 +++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 collector/pg_long_running_transactions.go create mode 100644 collector/pg_long_running_transactions_test.go diff --git a/collector/pg_long_running_transactions.go b/collector/pg_long_running_transactions.go new file mode 100644 index 000000000..baedd7869 --- /dev/null +++ b/collector/pg_long_running_transactions.go @@ -0,0 +1,93 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" +) + +const longRunningTransactionsSubsystem = "long_running_transactions" + +func init() { + registerCollector(longRunningTransactionsSubsystem, defaultDisabled, NewPGLongRunningTransactionsCollector) +} + +type PGLongRunningTransactionsCollector struct { + log log.Logger +} + +func NewPGLongRunningTransactionsCollector(config collectorConfig) (Collector, error) { + return &PGLongRunningTransactionsCollector{log: config.logger}, nil +} + +var ( + longRunningTransactionsCount = prometheus.NewDesc( + "pg_long_running_transactions", + "Current number of long running transactions", + []string{}, + prometheus.Labels{}, + ) + + longRunningTransactionsAgeInSeconds = prometheus.NewDesc( + prometheus.BuildFQName(namespace, longRunningTransactionsSubsystem, "age_in_seconds"), + "The current maximum transaction age in seconds", + []string{}, + prometheus.Labels{}, + ) + + longRunningTransactionsQuery = ` + SELECT + COUNT(*) as transactions, + MAX(EXTRACT(EPOCH FROM (clock_timestamp() - xact_start))) AS age_in_seconds + FROM pg_catalog.pg_stat_activity + WHERE state is distinct from 'idle' AND (now() - xact_start) > '1 minutes'::interval AND query not like 'autovacuum:%' + ` +) + +func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { + db := instance.getDB() + rows, err := db.QueryContext(ctx, + longRunningTransactionsQuery) + + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var transactions, ageInSeconds float64 + + if err := rows.Scan(&transactions, &ageInSeconds); err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric( + longRunningTransactionsCount, + prometheus.GaugeValue, + transactions, + ) + ch <- prometheus.MustNewConstMetric( + longRunningTransactionsAgeInSeconds, + prometheus.GaugeValue, + ageInSeconds, + ) + } + if err := rows.Err(); err != nil { + return err + } + return nil +} diff --git a/collector/pg_long_running_transactions_test.go b/collector/pg_long_running_transactions_test.go new file mode 100644 index 000000000..eedda7c65 --- /dev/null +++ b/collector/pg_long_running_transactions_test.go @@ -0,0 +1,63 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package collector + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/smartystreets/goconvey/convey" +) + +func TestPGLongRunningTransactionsCollector(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub db connection: %s", err) + } + defer db.Close() + inst := &instance{db: db} + columns := []string{ + "transactions", + "age_in_seconds", + } + rows := sqlmock.NewRows(columns). + AddRow(20, 1200) + + mock.ExpectQuery(sanitizeQuery(longRunningTransactionsQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGLongRunningTransactionsCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGLongRunningTransactionsCollector.Update: %s", err) + } + }() + expected := []MetricResult{ + {labels: labelMap{}, value: 20, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 1200, metricType: dto.MetricType_GAUGE}, + } + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, m) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} From cada317c294ed56c1f2698b4ce3b294596219fca Mon Sep 17 00:00:00 2001 From: Felix Yuan Date: Wed, 16 Aug 2023 09:55:50 -0700 Subject: [PATCH 2/4] Update collector/pg_long_running_transactions.go Co-authored-by: Ben Kochie Signed-off-by: Felix Yuan --- collector/pg_long_running_transactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collector/pg_long_running_transactions.go b/collector/pg_long_running_transactions.go index baedd7869..b4cefd064 100644 --- a/collector/pg_long_running_transactions.go +++ b/collector/pg_long_running_transactions.go @@ -43,7 +43,7 @@ var ( ) longRunningTransactionsAgeInSeconds = prometheus.NewDesc( - prometheus.BuildFQName(namespace, longRunningTransactionsSubsystem, "age_in_seconds"), + prometheus.BuildFQName(namespace, longRunningTransactionsSubsystem, "oldest_timestamp_seconds"), "The current maximum transaction age in seconds", []string{}, prometheus.Labels{}, From 88434b408a52bd03e3dd4553c1fb25df9be82877 Mon Sep 17 00:00:00 2001 From: Felix Yuan Date: Wed, 16 Aug 2023 09:55:55 -0700 Subject: [PATCH 3/4] Update collector/pg_long_running_transactions.go Co-authored-by: Ben Kochie Signed-off-by: Felix Yuan --- collector/pg_long_running_transactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collector/pg_long_running_transactions.go b/collector/pg_long_running_transactions.go index b4cefd064..dee589637 100644 --- a/collector/pg_long_running_transactions.go +++ b/collector/pg_long_running_transactions.go @@ -52,7 +52,7 @@ var ( longRunningTransactionsQuery = ` SELECT COUNT(*) as transactions, - MAX(EXTRACT(EPOCH FROM (clock_timestamp() - xact_start))) AS age_in_seconds + MAX(EXTRACT(EPOCH FROM clock_timestamp())) AS oldest_timestamp_seconds FROM pg_catalog.pg_stat_activity WHERE state is distinct from 'idle' AND (now() - xact_start) > '1 minutes'::interval AND query not like 'autovacuum:%' ` From 913817ad2e2b4171edce14336256b0a449c2ec77 Mon Sep 17 00:00:00 2001 From: Felix Yuan Date: Wed, 16 Aug 2023 10:35:30 -0700 Subject: [PATCH 4/4] Review fixes Signed-off-by: Felix Yuan --- collector/pg_long_running_transactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collector/pg_long_running_transactions.go b/collector/pg_long_running_transactions.go index dee589637..ffd89d5f0 100644 --- a/collector/pg_long_running_transactions.go +++ b/collector/pg_long_running_transactions.go @@ -54,7 +54,7 @@ var ( COUNT(*) as transactions, MAX(EXTRACT(EPOCH FROM clock_timestamp())) AS oldest_timestamp_seconds FROM pg_catalog.pg_stat_activity - WHERE state is distinct from 'idle' AND (now() - xact_start) > '1 minutes'::interval AND query not like 'autovacuum:%' + WHERE state is distinct from 'idle' AND query not like 'autovacuum:%' ` )