From 6d5f0011c4c11c8687ff204d6e3d154fc786f364 Mon Sep 17 00:00:00 2001 From: Joe Adams Date: Thu, 11 Dec 2025 21:04:35 -0500 Subject: [PATCH] Fix NULL on long_running_transactions collector When there are no long running transactions, the oldest timestamp will be NULL. This sets the metrics value to 0 (age). Also adds a test for this behavior. Fixes #1223 Signed-off-by: Joe Adams --- collector/pg_long_running_transactions.go | 15 +++++-- .../pg_long_running_transactions_test.go | 41 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/collector/pg_long_running_transactions.go b/collector/pg_long_running_transactions.go index 072862f4e..ac1eaa70e 100644 --- a/collector/pg_long_running_transactions.go +++ b/collector/pg_long_running_transactions.go @@ -15,6 +15,7 @@ package collector import ( "context" + "database/sql" "log/slog" "github.com/prometheus/client_golang/prometheus" @@ -50,7 +51,7 @@ var ( ) longRunningTransactionsQuery = ` - SELECT + SELECT COUNT(*) as transactions, MAX(EXTRACT(EPOCH FROM clock_timestamp() - pg_stat_activity.xact_start)) AS oldest_timestamp_seconds FROM pg_catalog.pg_stat_activity @@ -72,12 +73,20 @@ func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance * defer rows.Close() for rows.Next() { - var transactions, ageInSeconds float64 + var transactions float64 + var ageInSeconds sql.NullFloat64 if err := rows.Scan(&transactions, &ageInSeconds); err != nil { return err } + // If there are no long running transactions, ageInSeconds will be NULL + // so we set it to 0 + age := 0.0 + if ageInSeconds.Valid { + age = ageInSeconds.Float64 + } + ch <- prometheus.MustNewConstMetric( longRunningTransactionsCount, prometheus.GaugeValue, @@ -86,7 +95,7 @@ func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance * ch <- prometheus.MustNewConstMetric( longRunningTransactionsAgeInSeconds, prometheus.GaugeValue, - ageInSeconds, + age, ) } if err := rows.Err(); err != nil { diff --git a/collector/pg_long_running_transactions_test.go b/collector/pg_long_running_transactions_test.go index eedda7c65..ba7bc6a52 100644 --- a/collector/pg_long_running_transactions_test.go +++ b/collector/pg_long_running_transactions_test.go @@ -61,3 +61,44 @@ func TestPGLongRunningTransactionsCollector(t *testing.T) { t.Errorf("there were unfulfilled exceptions: %s", err) } } + +func TestPGLongRunningTransactionsCollectorNull(t *testing.T) { + // Test when no long running transactions are present + 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(0, nil) + + 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: 0, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 0, 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) + } +}