From 69c4119e19cf19ca933dd5980ed43b36b0dcd831 Mon Sep 17 00:00:00 2001 From: "Alexander J. Maidak" Date: Thu, 17 Oct 2024 09:43:55 -0500 Subject: [PATCH] Collect idle_in_transaction_time from PG >= 15 idle_in_transaction_time was added to pg_stat_database in pg 15 Signed-off-by: Alexander J. Maidak --- collector/pg_stat_database.go | 34 ++++++++- collector/pg_stat_database_test.go | 110 +++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/collector/pg_stat_database.go b/collector/pg_stat_database.go index b3cb59eb2..d4abdd153 100644 --- a/collector/pg_stat_database.go +++ b/collector/pg_stat_database.go @@ -218,6 +218,15 @@ var ( []string{"datid", "datname"}, prometheus.Labels{}, ) + statDatabaseIdleInTransaction = prometheus.NewDesc(prometheus.BuildFQName( + namespace, + statDatabaseSubsystem, + "idle_in_transaction_time_seconds_total", + ), + "Time spent idling while in a transaction in this database, in seconds", + []string{"datid", "datname"}, + prometheus.Labels{}, + ) ) func statDatabaseQuery(columns []string) string { @@ -254,6 +263,11 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance columns = append(columns, "active_time") } + idleInTransactionTimeAvail := instance.version.GTE(semver.MustParse("15.0.0")) + if idleInTransactionTimeAvail { + columns = append(columns, "idle_in_transaction_time") + } + rows, err := db.QueryContext(ctx, statDatabaseQuery(columns), ) @@ -264,7 +278,7 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance for rows.Next() { var datid, datname sql.NullString - var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime, activeTime sql.NullFloat64 + var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime, activeTime, idleInTransactionTime sql.NullFloat64 var statsReset sql.NullTime r := []any{ @@ -293,6 +307,10 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance r = append(r, &activeTime) } + if idleInTransactionTimeAvail { + r = append(r, &idleInTransactionTime) + } + err := rows.Scan(r...) if err != nil { return err @@ -375,6 +393,11 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance continue } + if idleInTransactionTimeAvail && !idleInTransactionTime.Valid { + level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no idle_in_transaction_time") + continue + } + statsResetMetric := 0.0 if !statsReset.Valid { level.Debug(c.log).Log("msg", "No metric for stats_reset, will collect 0 instead") @@ -512,6 +535,15 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance labels..., ) } + + if idleInTransactionTimeAvail { + ch <- prometheus.MustNewConstMetric( + statDatabaseIdleInTransaction, + prometheus.CounterValue, + idleInTransactionTime.Float64/1000.0, + labels..., + ) + } } return nil } diff --git a/collector/pg_stat_database_test.go b/collector/pg_stat_database_test.go index 924612082..7b61ad925 100644 --- a/collector/pg_stat_database_test.go +++ b/collector/pg_stat_database_test.go @@ -132,6 +132,116 @@ func TestPGStatDatabaseCollector(t *testing.T) { } } +func TestPGStatDatabaseCollectorCollectsIdleTime(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, version: semver.MustParse("15.0.0")} + + columns := []string{ + "datid", + "datname", + "numbackends", + "xact_commit", + "xact_rollback", + "blks_read", + "blks_hit", + "tup_returned", + "tup_fetched", + "tup_inserted", + "tup_updated", + "tup_deleted", + "conflicts", + "temp_files", + "temp_bytes", + "deadlocks", + "blk_read_time", + "blk_write_time", + "stats_reset", + "active_time", + "idle_in_transaction_time", + } + + srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07") + if err != nil { + t.Fatalf("Error parsing time: %s", err) + } + + rows := sqlmock.NewRows(columns). + AddRow( + "pid", + "postgres", + 354, + 4945, + 289097744, + 1242257, + int64(3275602074), + 89320867, + 450139, + 2034563757, + 0, + int64(2725688749), + 23, + 52, + 74, + 925, + 16, + 823, + srT, + 33, + 123, + ) + + mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGStatDatabaseCollector{ + log: log.With(log.NewNopLogger(), "collector", "pg_stat_database"), + } + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err) + } + }() + + expected := []MetricResult{ + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 354}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4945}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097744}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242257}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602074}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320867}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450139}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563757}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688749}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 23}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 52}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 74}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 925}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.033}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.123}, + } + + 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) + } +} + func TestPGStatDatabaseCollectorNullValues(t *testing.T) { db, mock, err := sqlmock.New() if err != nil {