From dfcfbb7f0f7eb61f9fda7f7c6714d6f6d8720efd Mon Sep 17 00:00:00 2001 From: Dean Chen <51218137+locker95@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:41:27 +0500 Subject: [PATCH] fix: skip autovacuum metric when xact_start is NULL (#1381) The stat_activity_autovacuum collector scanned xact_start into a plain float64, so a NULL value (possible for autovacuum workers) failed the whole collector with "converting NULL to float64 is unsupported". Scan into sql.NullFloat64 and skip the row when it is NULL, matching the pg_stat_walreceiver / pg_stat_database collectors. Fixes #952 Signed-off-by: Dean Chen <862469039@qq.com> --- collector/pg_stat_activity_autovacuum.go | 12 ++++-- collector/pg_stat_activity_autovacuum_test.go | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/collector/pg_stat_activity_autovacuum.go b/collector/pg_stat_activity_autovacuum.go index 40f43539..10350b5b 100644 --- a/collector/pg_stat_activity_autovacuum.go +++ b/collector/pg_stat_activity_autovacuum.go @@ -15,6 +15,7 @@ package collector import ( "context" + "database/sql" "log/slog" "github.com/prometheus/client_golang/prometheus" @@ -51,7 +52,7 @@ var ( ` ) -func (PGStatActivityAutovacuumCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { +func (c PGStatActivityAutovacuumCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { db := instance.getDB() rows, err := db.QueryContext(ctx, statActivityAutovacuumQuery) @@ -63,16 +64,21 @@ func (PGStatActivityAutovacuumCollector) Update(ctx context.Context, instance *i for rows.Next() { var relname string - var ageInSeconds float64 + var ageInSeconds sql.NullFloat64 if err := rows.Scan(&relname, &ageInSeconds); err != nil { return err } + if !ageInSeconds.Valid { + c.log.Debug("Skipping collecting metric because it has no xact_start") + continue + } + ch <- prometheus.MustNewConstMetric( statActivityAutovacuumAgeInSeconds, prometheus.GaugeValue, - ageInSeconds, relname, + ageInSeconds.Float64, relname, ) } if err := rows.Err(); err != nil { diff --git a/collector/pg_stat_activity_autovacuum_test.go b/collector/pg_stat_activity_autovacuum_test.go index a6fcdbca..667f2d73 100644 --- a/collector/pg_stat_activity_autovacuum_test.go +++ b/collector/pg_stat_activity_autovacuum_test.go @@ -19,6 +19,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -60,3 +61,43 @@ func TestPGStatActivityAutovacuumCollector(t *testing.T) { t.Errorf("there were unfulfilled exceptions: %s", err) } } + +func TestPGStatActivityAutovacuumCollectorNullTimestamp(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{ + "relname", + "timestamp_seconds", + } + rows := sqlmock.NewRows(columns). + AddRow("no_xact_start", nil). + AddRow("test", 3600) + + mock.ExpectQuery(sanitizeQuery(statActivityAutovacuumQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGStatActivityAutovacuumCollector{log: promslog.NewNopLogger()} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGStatActivityAutovacuumCollector.Update: %s", err) + } + }() + expected := []MetricResult{ + {labels: labelMap{"relname": "test"}, value: 3600, 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) + } +}