Skip to content

Commit 69c4119

Browse files
committed
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 <[email protected]>
1 parent 3be4edc commit 69c4119

File tree

2 files changed

+143
-1
lines changed

2 files changed

+143
-1
lines changed

collector/pg_stat_database.go

+33-1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ var (
218218
[]string{"datid", "datname"},
219219
prometheus.Labels{},
220220
)
221+
statDatabaseIdleInTransaction = prometheus.NewDesc(prometheus.BuildFQName(
222+
namespace,
223+
statDatabaseSubsystem,
224+
"idle_in_transaction_time_seconds_total",
225+
),
226+
"Time spent idling while in a transaction in this database, in seconds",
227+
[]string{"datid", "datname"},
228+
prometheus.Labels{},
229+
)
221230
)
222231

223232
func statDatabaseQuery(columns []string) string {
@@ -254,6 +263,11 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
254263
columns = append(columns, "active_time")
255264
}
256265

266+
idleInTransactionTimeAvail := instance.version.GTE(semver.MustParse("15.0.0"))
267+
if idleInTransactionTimeAvail {
268+
columns = append(columns, "idle_in_transaction_time")
269+
}
270+
257271
rows, err := db.QueryContext(ctx,
258272
statDatabaseQuery(columns),
259273
)
@@ -264,7 +278,7 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
264278

265279
for rows.Next() {
266280
var datid, datname sql.NullString
267-
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime, activeTime sql.NullFloat64
281+
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime, activeTime, idleInTransactionTime sql.NullFloat64
268282
var statsReset sql.NullTime
269283

270284
r := []any{
@@ -293,6 +307,10 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
293307
r = append(r, &activeTime)
294308
}
295309

310+
if idleInTransactionTimeAvail {
311+
r = append(r, &idleInTransactionTime)
312+
}
313+
296314
err := rows.Scan(r...)
297315
if err != nil {
298316
return err
@@ -375,6 +393,11 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
375393
continue
376394
}
377395

396+
if idleInTransactionTimeAvail && !idleInTransactionTime.Valid {
397+
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no idle_in_transaction_time")
398+
continue
399+
}
400+
378401
statsResetMetric := 0.0
379402
if !statsReset.Valid {
380403
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
512535
labels...,
513536
)
514537
}
538+
539+
if idleInTransactionTimeAvail {
540+
ch <- prometheus.MustNewConstMetric(
541+
statDatabaseIdleInTransaction,
542+
prometheus.CounterValue,
543+
idleInTransactionTime.Float64/1000.0,
544+
labels...,
545+
)
546+
}
515547
}
516548
return nil
517549
}

collector/pg_stat_database_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,116 @@ func TestPGStatDatabaseCollector(t *testing.T) {
132132
}
133133
}
134134

135+
func TestPGStatDatabaseCollectorCollectsIdleTime(t *testing.T) {
136+
db, mock, err := sqlmock.New()
137+
if err != nil {
138+
t.Fatalf("Error opening a stub db connection: %s", err)
139+
}
140+
defer db.Close()
141+
142+
inst := &instance{db: db, version: semver.MustParse("15.0.0")}
143+
144+
columns := []string{
145+
"datid",
146+
"datname",
147+
"numbackends",
148+
"xact_commit",
149+
"xact_rollback",
150+
"blks_read",
151+
"blks_hit",
152+
"tup_returned",
153+
"tup_fetched",
154+
"tup_inserted",
155+
"tup_updated",
156+
"tup_deleted",
157+
"conflicts",
158+
"temp_files",
159+
"temp_bytes",
160+
"deadlocks",
161+
"blk_read_time",
162+
"blk_write_time",
163+
"stats_reset",
164+
"active_time",
165+
"idle_in_transaction_time",
166+
}
167+
168+
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
169+
if err != nil {
170+
t.Fatalf("Error parsing time: %s", err)
171+
}
172+
173+
rows := sqlmock.NewRows(columns).
174+
AddRow(
175+
"pid",
176+
"postgres",
177+
354,
178+
4945,
179+
289097744,
180+
1242257,
181+
int64(3275602074),
182+
89320867,
183+
450139,
184+
2034563757,
185+
0,
186+
int64(2725688749),
187+
23,
188+
52,
189+
74,
190+
925,
191+
16,
192+
823,
193+
srT,
194+
33,
195+
123,
196+
)
197+
198+
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
199+
200+
ch := make(chan prometheus.Metric)
201+
go func() {
202+
defer close(ch)
203+
c := PGStatDatabaseCollector{
204+
log: log.With(log.NewNopLogger(), "collector", "pg_stat_database"),
205+
}
206+
207+
if err := c.Update(context.Background(), inst, ch); err != nil {
208+
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
209+
}
210+
}()
211+
212+
expected := []MetricResult{
213+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 354},
214+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4945},
215+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097744},
216+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242257},
217+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602074},
218+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320867},
219+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450139},
220+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563757},
221+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
222+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688749},
223+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 23},
224+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 52},
225+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 74},
226+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 925},
227+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
228+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
229+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
230+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.033},
231+
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.123},
232+
}
233+
234+
convey.Convey("Metrics comparison", t, func() {
235+
for _, expect := range expected {
236+
m := readMetric(<-ch)
237+
convey.So(expect, convey.ShouldResemble, m)
238+
}
239+
})
240+
if err := mock.ExpectationsWereMet(); err != nil {
241+
t.Errorf("there were unfulfilled exceptions: %s", err)
242+
}
243+
}
244+
135245
func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
136246
db, mock, err := sqlmock.New()
137247
if err != nil {

0 commit comments

Comments
 (0)