Skip to content

Commit 7786601

Browse files
authored
Fix NULL on long_running_transactions collector (#1230)
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 <github@joeadams.io>
1 parent d613418 commit 7786601

File tree

2 files changed

+53
-3
lines changed

2 files changed

+53
-3
lines changed

collector/pg_long_running_transactions.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package collector
1515

1616
import (
1717
"context"
18+
"database/sql"
1819
"log/slog"
1920

2021
"github.com/prometheus/client_golang/prometheus"
@@ -50,7 +51,7 @@ var (
5051
)
5152

5253
longRunningTransactionsQuery = `
53-
SELECT
54+
SELECT
5455
COUNT(*) as transactions,
5556
MAX(EXTRACT(EPOCH FROM clock_timestamp() - pg_stat_activity.xact_start)) AS oldest_timestamp_seconds
5657
FROM pg_catalog.pg_stat_activity
@@ -72,12 +73,20 @@ func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance *
7273
defer rows.Close()
7374

7475
for rows.Next() {
75-
var transactions, ageInSeconds float64
76+
var transactions float64
77+
var ageInSeconds sql.NullFloat64
7678

7779
if err := rows.Scan(&transactions, &ageInSeconds); err != nil {
7880
return err
7981
}
8082

83+
// If there are no long running transactions, ageInSeconds will be NULL
84+
// so we set it to 0
85+
age := 0.0
86+
if ageInSeconds.Valid {
87+
age = ageInSeconds.Float64
88+
}
89+
8190
ch <- prometheus.MustNewConstMetric(
8291
longRunningTransactionsCount,
8392
prometheus.GaugeValue,
@@ -86,7 +95,7 @@ func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance *
8695
ch <- prometheus.MustNewConstMetric(
8796
longRunningTransactionsAgeInSeconds,
8897
prometheus.GaugeValue,
89-
ageInSeconds,
98+
age,
9099
)
91100
}
92101
if err := rows.Err(); err != nil {

collector/pg_long_running_transactions_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,44 @@ func TestPGLongRunningTransactionsCollector(t *testing.T) {
6161
t.Errorf("there were unfulfilled exceptions: %s", err)
6262
}
6363
}
64+
65+
func TestPGLongRunningTransactionsCollectorNull(t *testing.T) {
66+
// Test when no long running transactions are present
67+
db, mock, err := sqlmock.New()
68+
if err != nil {
69+
t.Fatalf("Error opening a stub db connection: %s", err)
70+
}
71+
defer db.Close()
72+
inst := &instance{db: db}
73+
columns := []string{
74+
"transactions",
75+
"age_in_seconds",
76+
}
77+
rows := sqlmock.NewRows(columns).
78+
AddRow(0, nil)
79+
80+
mock.ExpectQuery(sanitizeQuery(longRunningTransactionsQuery)).WillReturnRows(rows)
81+
82+
ch := make(chan prometheus.Metric)
83+
go func() {
84+
defer close(ch)
85+
c := PGLongRunningTransactionsCollector{}
86+
87+
if err := c.Update(context.Background(), inst, ch); err != nil {
88+
t.Errorf("Error calling PGLongRunningTransactionsCollector.Update: %s", err)
89+
}
90+
}()
91+
expected := []MetricResult{
92+
{labels: labelMap{}, value: 0, metricType: dto.MetricType_GAUGE},
93+
{labels: labelMap{}, value: 0, metricType: dto.MetricType_GAUGE},
94+
}
95+
convey.Convey("Metrics comparison", t, func() {
96+
for _, expect := range expected {
97+
m := readMetric(<-ch)
98+
convey.So(expect, convey.ShouldResemble, m)
99+
}
100+
})
101+
if err := mock.ExpectationsWereMet(); err != nil {
102+
t.Errorf("there were unfulfilled exceptions: %s", err)
103+
}
104+
}

0 commit comments

Comments
 (0)