diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 17e4494f3..969abeda2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,11 +14,12 @@ jobs: strategy: matrix: postgresql-image: - - postgres:10 - - postgres:11 - - postgres:12 - postgres:13 - postgres:14 + - postgres:15 + - postgres:16 + - postgres:17 + - postgres:18 runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8f898f6..d214f699f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [Unreleased] + +* [ENHANCEMENT] Add PostgreSQL 18 support: + * Add parallel worker activity metrics + * Add vacuum/analyze timing metrics + * Add enhanced checkpointer metrics + * Add `pg_stat_io` collector with byte statistics and WAL I/O activity tracking +* [ENHANCEMENT] Update CI tested PostgreSQL versions to include PostgreSQL 18 + ## 0.15.0 / 2023-10-27 * [ENHANCEMENT] Add 1kB and 2kB units #915 diff --git a/README.md b/README.md index 429058e6d..de712d68b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Prometheus exporter for PostgreSQL server metrics. -CI Tested PostgreSQL versions: `11`, `12`, `13`, `14`, `15`, `16` +CI Tested PostgreSQL versions: `13`, `14`, `15`, `16`, `18` ## Quick Start This package is available for Docker: diff --git a/collector/pg_stat_bgwriter.go b/collector/pg_stat_bgwriter.go index 0b73b4f44..3fac6ee0f 100644 --- a/collector/pg_stat_bgwriter.go +++ b/collector/pg_stat_bgwriter.go @@ -16,6 +16,7 @@ package collector import ( "context" "database/sql" + "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" ) @@ -46,6 +47,12 @@ var ( []string{"collector", "server"}, prometheus.Labels{}, ) + statBGWriterCheckpointsDoneDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoints_done_total"), + "Number of completed checkpoints", + []string{"collector", "server"}, + prometheus.Labels{}, + ) statBGWriterCheckpointsReqTimeDesc = prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoint_write_time_total"), "Total amount of time that has been spent in the portion of checkpoint processing where files are written to disk, in milliseconds", @@ -94,6 +101,12 @@ var ( []string{"collector", "server"}, prometheus.Labels{}, ) + statBGWriterCheckpointsSlruWrittenDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, bgWriterSubsystem, "slru_written_total"), + "Number of SLRU buffers written during checkpoints and restartpoints", + []string{"collector", "server"}, + prometheus.Labels{}, + ) statBGWriterStatsResetDesc = prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "stats_reset_total"), "Time at which these statistics were last reset", @@ -114,6 +127,12 @@ var statBGWriter = map[string]*prometheus.Desc{ []string{"collector", "server"}, prometheus.Labels{}, ), + "percona_checkpoints_done": prometheus.NewDesc( + prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoints_done"), + "Number of completed checkpoints", + []string{"collector", "server"}, + prometheus.Labels{}, + ), "percona_checkpoint_write_time": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoint_write_time"), "Total amount of time that has been spent in the portion of checkpoint processing where files are written to disk, in milliseconds", @@ -162,6 +181,12 @@ var statBGWriter = map[string]*prometheus.Desc{ []string{"collector", "server"}, prometheus.Labels{}, ), + "percona_slru_written": prometheus.NewDesc( + prometheus.BuildFQName(namespace, bgWriterSubsystem, "slru_written"), + "Number of SLRU buffers written during checkpoints and restartpoints", + []string{"collector", "server"}, + prometheus.Labels{}, + ), "percona_stats_reset": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "stats_reset"), "Time at which these statistics were last reset", @@ -191,25 +216,43 @@ const statBGWriterQueryPost17 = `SELECT ,stats_reset FROM pg_stat_bgwriter;` -const statCheckpointerQuery = `SELECT +const statCheckpointerQueryPre18 = `SELECT + num_timed + ,num_requested + ,NULL::bigint as num_done + ,restartpoints_timed + ,restartpoints_req + ,restartpoints_done + ,write_time + ,sync_time + ,buffers_written + ,NULL::bigint as slru_written + ,stats_reset + FROM pg_stat_checkpointer;` + +const statCheckpointerQuery18Plus = `SELECT num_timed ,num_requested + ,num_done ,restartpoints_timed ,restartpoints_req ,restartpoints_done ,write_time ,sync_time ,buffers_written + ,slru_written ,stats_reset FROM pg_stat_checkpointer;` func (p PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { db := instance.getDB() - var cpt, cpr, bcp, bc, mwc, bb, bbf, ba sql.NullInt64 + var cpt, cpr, cpd, bcp, bc, mwc, bb, bbf, ba, slruw sql.NullInt64 var cpwt, cpst sql.NullFloat64 var sr sql.NullTime + after18 := instance.version.GTE(semver.Version{Major: 18}) + if instance.version.GE(semver.MustParse("17.0.0")) { row := db.QueryRowContext(ctx, statBGWriterQueryPost17) @@ -219,10 +262,15 @@ func (p PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, } var rpt, rpr, rpd sql.NullInt64 var csr sql.NullTime - // these variables are not used, but I left them here for reference - row = db.QueryRowContext(ctx, - statCheckpointerQuery) - err = row.Scan(&cpt, &cpr, &rpt, &rpr, &rpd, &cpwt, &cpst, &bcp, &csr) + + // Use version-specific checkpointer query for PostgreSQL 18+ + checkpointerQuery := statCheckpointerQueryPre18 + if after18 { + checkpointerQuery = statCheckpointerQuery18Plus + } + + row = db.QueryRowContext(ctx, checkpointerQuery) + err = row.Scan(&cpt, &cpr, &cpd, &rpt, &rpr, &rpd, &cpwt, &cpst, &bcp, &slruw, &csr) if err != nil { return err } @@ -257,6 +305,21 @@ func (p PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, "exporter", instance.name, ) + + cpdMetric := 0.0 + if after18 { + if cpd.Valid { + cpdMetric = float64(cpd.Int64) + } + ch <- prometheus.MustNewConstMetric( + statBGWriterCheckpointsDoneDesc, + prometheus.CounterValue, + cpdMetric, + "exporter", + instance.name, + ) + } + cpwtMetric := 0.0 if cpwt.Valid { cpwtMetric = float64(cpwt.Float64) @@ -345,6 +408,19 @@ func (p PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, "exporter", instance.name, ) + slruwMetric := 0.0 + if after18 { + if slruw.Valid { + slruwMetric = float64(slruw.Int64) + } + ch <- prometheus.MustNewConstMetric( + statBGWriterCheckpointsSlruWrittenDesc, + prometheus.CounterValue, + slruwMetric, + "exporter", + instance.name, + ) + } srMetric := 0.0 if sr.Valid { srMetric = float64(sr.Time.Unix()) @@ -373,6 +449,15 @@ func (p PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, "exporter", instance.name, ) + if after18 { + ch <- prometheus.MustNewConstMetric( + statBGWriter["percona_checkpoints_done"], + prometheus.CounterValue, + cpdMetric, + "exporter", + instance.name, + ) + } ch <- prometheus.MustNewConstMetric( statBGWriter["percona_checkpoint_write_time"], prometheus.CounterValue, @@ -429,6 +514,15 @@ func (p PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, "exporter", instance.name, ) + if after18 { + ch <- prometheus.MustNewConstMetric( + statBGWriter["percona_slru_written"], + prometheus.CounterValue, + slruwMetric, + "exporter", + instance.name, + ) + } ch <- prometheus.MustNewConstMetric( statBGWriter["percona_stats_reset"], prometheus.CounterValue, diff --git a/collector/pg_stat_database.go b/collector/pg_stat_database.go index 328afee2c..1afc93ed4 100644 --- a/collector/pg_stat_database.go +++ b/collector/pg_stat_database.go @@ -17,6 +17,7 @@ import ( "context" "database/sql" + "github.com/blang/semver/v4" "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" @@ -206,8 +207,52 @@ var ( []string{"datid", "datname"}, prometheus.Labels{}, ) + statDatabaseParallelWorkersToLaunch = prometheus.NewDesc(prometheus.BuildFQName( + namespace, + statDatabaseSubsystem, + "parallel_workers_to_launch", + ), + "Number of parallel workers to launch (PostgreSQL 18+)", + []string{"datid", "datname"}, + prometheus.Labels{}, + ) + statDatabaseParallelWorkersLaunched = prometheus.NewDesc(prometheus.BuildFQName( + namespace, + statDatabaseSubsystem, + "parallel_workers_launched", + ), + "Number of parallel workers launched (PostgreSQL 18+)", + []string{"datid", "datname"}, + prometheus.Labels{}, + ) + + statDatabaseQueryPrePG18 = ` + SELECT + 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 + ,NULL::bigint as parallel_workers_to_launch + ,NULL::bigint as parallel_workers_launched + FROM pg_stat_database; + ` - statDatabaseQuery = ` + statDatabaseQueryPG18 = ` SELECT datid ,datname @@ -228,15 +273,23 @@ var ( ,blk_read_time ,blk_write_time ,stats_reset + ,parallel_workers_to_launch + ,parallel_workers_launched FROM pg_stat_database; ` ) func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { db := instance.getDB() - rows, err := db.QueryContext(ctx, - statDatabaseQuery, - ) + + after18 := instance.version.GTE(semver.Version{Major: 18}) + // Use version-specific query for PostgreSQL 18+ + query := statDatabaseQueryPrePG18 + if after18 { + query = statDatabaseQueryPG18 + } + + rows, err := db.QueryContext(ctx, query) if err != nil { return err } @@ -246,6 +299,7 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance var datid, datname sql.NullString var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime sql.NullFloat64 var statsReset sql.NullTime + var parallelWorkersToLaunch, parallelWorkersLaunched sql.NullFloat64 err := rows.Scan( &datid, @@ -267,6 +321,8 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance &blkReadTime, &blkWriteTime, &statsReset, + ¶llelWorkersToLaunch, + ¶llelWorkersLaunched, ) if err != nil { return err @@ -353,6 +409,18 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance statsResetMetric = float64(statsReset.Time.Unix()) } + if after18 { + if !parallelWorkersToLaunch.Valid && after18 { + level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no parallel_workers_to_launch") + continue + } + + if !parallelWorkersLaunched.Valid { + level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no parallel_workers_launched") + continue + } + } + labels := []string{datid.String, datname.String} ch <- prometheus.MustNewConstMetric( @@ -473,6 +541,22 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance statsResetMetric, labels..., ) + + if after18 { + ch <- prometheus.MustNewConstMetric( + statDatabaseParallelWorkersToLaunch, + prometheus.CounterValue, + parallelWorkersToLaunch.Float64, + labels..., + ) + + ch <- prometheus.MustNewConstMetric( + statDatabaseParallelWorkersLaunched, + prometheus.CounterValue, + parallelWorkersLaunched.Float64, + labels..., + ) + } } return nil } diff --git a/collector/pg_stat_database_test.go b/collector/pg_stat_database_test.go index fe1b17066..cb82814c2 100644 --- a/collector/pg_stat_database_test.go +++ b/collector/pg_stat_database_test.go @@ -18,12 +18,15 @@ import ( "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/blang/semver/v4" "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/smartystreets/goconvey/convey" ) +var pg18 = semver.MustParse("18.0.0") + func TestPGStatDatabaseCollector(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { @@ -31,7 +34,7 @@ func TestPGStatDatabaseCollector(t *testing.T) { } defer db.Close() - inst := &instance{db: db} + inst := &instance{db: db, version: pg18} columns := []string{ "datid", @@ -53,6 +56,8 @@ func TestPGStatDatabaseCollector(t *testing.T) { "blk_read_time", "blk_write_time", "stats_reset", + "parallel_workers_to_launch", + "parallel_workers_launched", } srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07") @@ -80,9 +85,12 @@ func TestPGStatDatabaseCollector(t *testing.T) { 925, 16, 823, - srT) + srT, + 3, + 2, + ) - mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows) + mock.ExpectQuery(sanitizeQuery(statDatabaseQueryPG18)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { @@ -114,6 +122,8 @@ func TestPGStatDatabaseCollector(t *testing.T) { {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: 3}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2}, } convey.Convey("Metrics comparison", t, func() { @@ -138,7 +148,7 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) { if err != nil { t.Fatalf("Error parsing time: %s", err) } - inst := &instance{db: db} + inst := &instance{db: db, version: pg18} columns := []string{ "datid", @@ -160,6 +170,8 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) { "blk_read_time", "blk_write_time", "stats_reset", + "parallel_workers_to_launch", + "parallel_workers_launched", } rows := sqlmock.NewRows(columns). @@ -182,7 +194,10 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) { 925, 16, 823, - srT). + srT, + nil, + nil, + ). AddRow( "pid", "postgres", @@ -202,8 +217,11 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) { 925, 16, 823, - srT) - mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows) + srT, + 3, + 2, + ) + mock.ExpectQuery(sanitizeQuery(statDatabaseQueryPG18)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { @@ -235,6 +253,8 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) { {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: 3}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2}, } convey.Convey("Metrics comparison", t, func() { @@ -254,7 +274,7 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { } defer db.Close() - inst := &instance{db: db} + inst := &instance{db: db, version: pg18} columns := []string{ "datid", @@ -276,6 +296,8 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { "blk_read_time", "blk_write_time", "stats_reset", + "parallel_workers_to_launch", + "parallel_workers_launched", } srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07") @@ -303,7 +325,10 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { 925, 16, 823, - srT). + srT, + 5, + 4, + ). AddRow( nil, nil, @@ -324,6 +349,8 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { nil, nil, nil, + nil, + nil, ). AddRow( "pid", @@ -344,8 +371,11 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { 926, 17, 824, - srT) - mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows) + srT, + 3, + 2, + ) + mock.ExpectQuery(sanitizeQuery(statDatabaseQueryPG18)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { @@ -377,6 +407,8 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { {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: 5}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4}, {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 355}, {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4946}, {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097745}, @@ -394,6 +426,8 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) { {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 17}, {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 824}, {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2}, } convey.Convey("Metrics comparison", t, func() { @@ -414,7 +448,7 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) { } defer db.Close() - inst := &instance{db: db} + inst := &instance{db: db, version: pg18} columns := []string{ "datid", @@ -436,6 +470,8 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) { "blk_read_time", "blk_write_time", "stats_reset", + "parallel_workers_to_launch", + "parallel_workers_launched", } rows := sqlmock.NewRows(columns). @@ -458,9 +494,12 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) { 925, 16, 823, - nil) + nil, + 3, + 2, + ) - mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows) + mock.ExpectQuery(sanitizeQuery(statDatabaseQueryPG18)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { @@ -492,6 +531,8 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) { {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: 0}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3}, + {labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2}, } convey.Convey("Metrics comparison", t, func() { diff --git a/collector/pg_stat_io.go b/collector/pg_stat_io.go new file mode 100644 index 000000000..053826926 --- /dev/null +++ b/collector/pg_stat_io.go @@ -0,0 +1,429 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "database/sql" + + "github.com/blang/semver/v4" + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" +) + +const statIOSubsystem = "stat_io" + +func init() { + registerCollector(statIOSubsystem, defaultDisabled, NewPGStatIOCollector) +} + +type PGStatIOCollector struct { + log log.Logger +} + +func NewPGStatIOCollector(config collectorConfig) (Collector, error) { + return &PGStatIOCollector{log: config.logger}, nil +} + +var ( + statIOReads = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "reads_total"), + "Number of read operations", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOReadBytes = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "read_bytes_total"), + "The total size of read operations, in bytes", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOReadTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "read_time_total"), + "Time spent waiting for read operations, in milliseconds", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOWrites = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "writes_total"), + "Number of write operations", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOWriteBytes = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "write_bytes_total"), + "The total size of write operations, in bytes", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOWriteTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "write_time_total"), + "Time spent waiting for write operations, in milliseconds", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOWritebacks = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "writebacks_total"), + "Number of units of size BLCKSZ (typically 8kB) which the process requested the kernel write out to permanent storage", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOWritebackTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "writeback_time_total"), + "Time spent waiting for writeback operations, in milliseconds", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOExtends = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "extends_total"), + "Number of extend operations", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOExtendBytes = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "extend_bytes_total"), + "The total size of extend operations, in bytes", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOExtendTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "extend_time_total"), + "Time spent waiting for extend operations, in milliseconds", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOHits = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "hits_total"), + "The number of times a desired block was found in a shared buffer", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOEvictions = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "evictions_total"), + "Number of times a block has been written out from a shared or local buffer in order to make it available for another use", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOReueses = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "reueses_total"), + "The number of times an existing buffer in a size-limited ring buffer outside of shared buffers was reused as part of an I/O operation in the bulkread, bulkwrite, or vacuum contexts", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOFsyncs = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "fsyncs_total"), + "Number of fsync calls", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + statIOFsyncTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, statIOSubsystem, "fsync_time_total"), + "Time spent waiting for fsync operations, in milliseconds", + []string{"backend_type", "io_context", "io_object"}, + prometheus.Labels{}, + ) + + // PostgreSQL 18+ query with byte statistics and WAL I/O + StatIOQuery18Plus = ` + SELECT + backend_type, + io_object, + io_context, + reads, + read_bytes, + read_time, + writes, + write_bytes, + write_time, + writebacks, + writeback_time, + extends, + extend_bytes, + extend_time, + hits, + evictions, + reueses, + fsyncs + fsync_time, + FROM pg_stat_io + ` + + // Pre-PostgreSQL 18 query without byte statistics + StatIOQueryPre18 = ` + SELECT + backend_type, + io_object, + io_context, + reads, + NULL::bigint as read_bytes, + read_time, + writes, + NULL::bigint as write_bytes, + write_time, + writebacks, + writeback_time, + extends, + NULL::numeric as extend_bytes, + extend_time, + hits, + NULL::bigint as evictions, + reueses, + fsyncs + fsync_time + FROM pg_stat_io + ` +) + +func (c *PGStatIOCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { + // pg_stat_io was introduced in PostgreSQL 16 + if instance.version.LT(semver.Version{Major: 16}) { + return nil + } + + db := instance.getDB() + + after18 := instance.version.GTE(semver.Version{Major: 18}) + // Use version-specific query for PostgreSQL 18+ + query := StatIOQueryPre18 + if after18 { + query = StatIOQuery18Plus + } + + rows, err := db.QueryContext(ctx, query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var backendType, ioContext, ioObject sql.NullString + var reads, writes, writebacks, extends, hits, evictions, reueses, fsyncs sql.NullInt64 + var readBytes, writeBytes, extendBytes, readTime, writeTime, extendTime, writebackTime, fsyncTime sql.NullFloat64 + + err := rows.Scan( + &backendType, + &ioObject, + &ioContext, + &reads, + &readBytes, + &readTime, + &writes, + &writeBytes, + &writeTime, + &writebacks, + &writebackTime, + &extends, + &extendBytes, + &extendTime, + &hits, + &evictions, + &reueses, + &fsyncs, + &fsyncTime, + ) + if err != nil { + return err + } + + backendTypeLabel := "unknown" + if backendType.Valid { + backendTypeLabel = backendType.String + } + ioObjectLabel := "unknown" + if ioObject.Valid { + ioObjectLabel = ioObject.String + } + ioContextLabel := "unknown" + if ioContext.Valid { + ioContextLabel = ioContext.String + } + + labels := []string{backendTypeLabel, ioContextLabel, ioObjectLabel} + + readsMetric := 0.0 + if reads.Valid { + readsMetric = float64(reads.Int64) + } + ch <- prometheus.MustNewConstMetric( + statIOReads, + prometheus.CounterValue, + readsMetric, + labels..., + ) + + if readTime.Valid { + ch <- prometheus.MustNewConstMetric( + statIOReadTime, + prometheus.CounterValue, + readTime.Float64, + labels..., + ) + } + + writesMetric := 0.0 + if writes.Valid { + writesMetric = float64(writes.Int64) + } + ch <- prometheus.MustNewConstMetric( + statIOWrites, + prometheus.CounterValue, + writesMetric, + labels..., + ) + + if writeTime.Valid { + ch <- prometheus.MustNewConstMetric( + statIOWriteTime, + prometheus.CounterValue, + writeTime.Float64, + labels..., + ) + } + + writebacksMetric := 0.0 + if writebacks.Valid { + writebacksMetric = float64(writebacks.Int64) + } + if writebacks.Valid { + ch <- prometheus.MustNewConstMetric( + statIOWritebacks, + prometheus.CounterValue, + writebacksMetric, + labels..., + ) + } + + if writebackTime.Valid { + ch <- prometheus.MustNewConstMetric( + statIOWritebackTime, + prometheus.CounterValue, + writebackTime.Float64, + labels..., + ) + } + + extendsMetric := 0.0 + if extends.Valid { + extendsMetric = float64(extends.Int64) + } + if extends.Valid { + ch <- prometheus.MustNewConstMetric( + statIOExtends, + prometheus.CounterValue, + extendsMetric, + labels..., + ) + } + + if extendTime.Valid { + ch <- prometheus.MustNewConstMetric( + statIOExtendTime, + prometheus.CounterValue, + extendTime.Float64, + labels..., + ) + } + + hitsMetric := 0.0 + if hits.Valid { + hitsMetric = float64(hits.Int64) + } + if hits.Valid { + ch <- prometheus.MustNewConstMetric( + statIOHits, + prometheus.CounterValue, + hitsMetric, + labels..., + ) + } + evictionsMetric := 0.0 + if evictions.Valid { + evictionsMetric = float64(evictions.Int64) + } + if evictions.Valid { + ch <- prometheus.MustNewConstMetric( + statIOEvictions, + prometheus.CounterValue, + evictionsMetric, + labels..., + ) + } + reuesesMetric := 0.0 + if reueses.Valid { + reuesesMetric = float64(reueses.Int64) + } + if reueses.Valid { + ch <- prometheus.MustNewConstMetric( + statIOReueses, + prometheus.CounterValue, + reuesesMetric, + labels..., + ) + } + + fsyncsMetric := 0.0 + if fsyncs.Valid { + fsyncsMetric = float64(fsyncs.Int64) + } + if fsyncs.Valid { + ch <- prometheus.MustNewConstMetric( + statIOFsyncs, + prometheus.CounterValue, + fsyncsMetric, + labels..., + ) + } + + if fsyncTime.Valid { + ch <- prometheus.MustNewConstMetric( + statIOFsyncTime, + prometheus.CounterValue, + fsyncTime.Float64, + labels..., + ) + } + + // PostgreSQL 18+ byte statistics + if after18 { + if readBytes.Valid { + ch <- prometheus.MustNewConstMetric( + statIOReadBytes, + prometheus.CounterValue, + readBytes.Float64, + labels..., + ) + } + + if writeBytes.Valid { + ch <- prometheus.MustNewConstMetric( + statIOWriteBytes, + prometheus.CounterValue, + writeBytes.Float64, + labels..., + ) + } + + if extendBytes.Valid { + ch <- prometheus.MustNewConstMetric( + statIOExtendBytes, + prometheus.CounterValue, + extendBytes.Float64, + labels..., + ) + } + } + } + + return nil +} diff --git a/collector/pg_stat_io_test.go b/collector/pg_stat_io_test.go new file mode 100644 index 000000000..0071c902d --- /dev/null +++ b/collector/pg_stat_io_test.go @@ -0,0 +1,161 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/blang/semver/v4" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/smartystreets/goconvey/convey" +) + +func TestPGStatIOCollector(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &instance{db: db, version: semver.MustParse("16.0.0")} + + columns := []string{"backend_type", "io_object", "io_context", "reads", "read_bytes", "read_time", "writes", "write_bytes", "write_time", "writebacks", "writeback_time", "extends", "extend_bytes", "extend_time", "hits", "evictions", "reueses", "fsyncs", "fsync_time"} + rows := sqlmock.NewRows(columns). + AddRow("client backend", "relation", "normal", 100, nil, 50.5, 75, nil, 25.2, 10, 12.0, 7, nil, 11.0, 1, 2, 3, 4, 8.0) + mock.ExpectQuery("SELECT.*backend_type.*FROM pg_stat_io").WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGStatIOCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGStatIOCollector.Update: %s", err) + } + }() + + labels := labelMap{"backend_type": "client backend", "io_object": "relation", "io_context": "normal"} + expected := []MetricResult{ + {labels: labels, metricType: dto.MetricType_COUNTER, value: 100}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 50.5}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 75}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 25.2}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 10}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 12.0}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 7}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 11.0}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 1}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 2}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 3}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 4}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 8.0}, + } + + 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 TestPGStatIOCollectorPostgreSQL18(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &instance{db: db, version: pg18} + + columns := []string{"backend_type", "io_context", "io_object", "reads", "read_bytes", "read_time", "writes", "write_bytes", "write_time", "writebacks", "writeback_time", "extends", "extend_bytes", "extend_time", "hits", "evictions", "reueses", "fsyncs", "fsync_time"} + rows := sqlmock.NewRows(columns). + AddRow("client backend", "relation", "normal", 100, 90, 50.5, 75, 80, 25.2, 10, 12.0, 7, 30, 11.0, 1, 2, 3, 4, 8.0) + mock.ExpectQuery("SELECT.*backend_type.*FROM pg_stat_io").WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGStatIOCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGStatIOCollector.Update: %s", err) + } + }() + + labels := labelMap{"backend_type": "client backend", "io_object": "relation", "io_context": "normal"} + expected := []MetricResult{ + {labels: labels, metricType: dto.MetricType_COUNTER, value: 100}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 50.5}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 75}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 25.2}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 10}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 12.0}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 7}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 11.0}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 1}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 2}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 3}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 4}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 8.0}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 90}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 80}, + {labels: labels, metricType: dto.MetricType_COUNTER, value: 30}, + } + + 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 TestPGStatIOCollectorPrePostgreSQL16(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub database connection: %s", err) + } + defer db.Close() + + inst := &instance{db: db, version: semver.MustParse("15.0.0")} + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGStatIOCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGStatIOCollector.Update: %s", err) + } + }() + + // Should not make any queries for PostgreSQL < 16 + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("There were unfulfilled expectations: %s", err) + } + + for range ch { + t.Error("Don't expect any metrics for PostgreSQL < 16") + } +} diff --git a/collector/pg_stat_user_tables.go b/collector/pg_stat_user_tables.go index af3822ca8..18b48593b 100644 --- a/collector/pg_stat_user_tables.go +++ b/collector/pg_stat_user_tables.go @@ -17,6 +17,7 @@ import ( "context" "database/sql" + "github.com/blang/semver/v4" "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -156,8 +157,63 @@ var ( []string{"datname", "schemaname", "relname"}, prometheus.Labels{}, ) + statUserTablesTotalVacuumTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, userTableSubsystem, "total_vacuum_time"), + "Time spent vacuuming this table, in milliseconds (PostgreSQL 18+)", + []string{"datname", "schemaname", "relname"}, + prometheus.Labels{}, + ) + statUserTablesTotalAutovacuumTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, userTableSubsystem, "total_autovacuum_time"), + "Time spent auto-vacuuming this table, in milliseconds (PostgreSQL 18+)", + []string{"datname", "schemaname", "relname"}, + prometheus.Labels{}, + ) + statUserTablesTotalAnalyzeTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, userTableSubsystem, "total_analyze_time"), + "Time spent analyzing this table, in milliseconds (PostgreSQL 18+)", + []string{"datname", "schemaname", "relname"}, + prometheus.Labels{}, + ) + statUserTablesTotalAutoanalyzeTime = prometheus.NewDesc( + prometheus.BuildFQName(namespace, userTableSubsystem, "total_autoanalyze_time"), + "Time spent auto-analyzing this table, in milliseconds (PostgreSQL 18+)", + []string{"datname", "schemaname", "relname"}, + prometheus.Labels{}, + ) + + statUserTablesQueryPrePG18 = `SELECT + current_database() datname, + schemaname, + relname, + seq_scan, + seq_tup_read, + idx_scan, + idx_tup_fetch, + n_tup_ins, + n_tup_upd, + n_tup_del, + n_tup_hot_upd, + n_live_tup, + n_dead_tup, + n_mod_since_analyze, + COALESCE(last_vacuum, '1970-01-01Z') as last_vacuum, + COALESCE(last_autovacuum, '1970-01-01Z') as last_autovacuum, + COALESCE(last_analyze, '1970-01-01Z') as last_analyze, + COALESCE(last_autoanalyze, '1970-01-01Z') as last_autoanalyze, + vacuum_count, + autovacuum_count, + analyze_count, + autoanalyze_count, + pg_total_relation_size(relid) as total_size, + NULL::double precision as total_vacuum_time, + NULL::double precision as total_autovacuum_time, + NULL::double precision as total_analyze_time, + NULL::double precision as total_autoanalyze_time + FROM + pg_stat_user_tables` - statUserTablesQuery = `SELECT + statUserTablesQueryPG18 = `SELECT current_database() datname, schemaname, relname, @@ -180,15 +236,26 @@ var ( autovacuum_count, analyze_count, autoanalyze_count, - pg_total_relation_size(relid) as total_size + pg_total_relation_size(relid) as total_size, + total_vacuum_time, + total_autovacuum_time, + total_analyze_time, + total_autoanalyze_time FROM pg_stat_user_tables` ) func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { db := instance.getDB() - rows, err := db.QueryContext(ctx, - statUserTablesQuery) + + after18 := instance.version.GTE(semver.Version{Major: 18}) + // Use version-specific query for PostgreSQL 18+ + query := statUserTablesQueryPrePG18 + if after18 { + query = statUserTablesQueryPG18 + } + + rows, err := db.QueryContext(ctx, query) if err != nil { return err @@ -200,8 +267,9 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan var seqScan, seqTupRead, idxScan, idxTupFetch, nTupIns, nTupUpd, nTupDel, nTupHotUpd, nLiveTup, nDeadTup, nModSinceAnalyze, vacuumCount, autovacuumCount, analyzeCount, autoanalyzeCount, totalSize sql.NullInt64 var lastVacuum, lastAutovacuum, lastAnalyze, lastAutoanalyze sql.NullTime + var totalVacuumTime, totalAutovacuumTime, totalAnalyzeTime, totalAutoanalyzeTime sql.NullFloat64 - if err := rows.Scan(&datname, &schemaname, &relname, &seqScan, &seqTupRead, &idxScan, &idxTupFetch, &nTupIns, &nTupUpd, &nTupDel, &nTupHotUpd, &nLiveTup, &nDeadTup, &nModSinceAnalyze, &lastVacuum, &lastAutovacuum, &lastAnalyze, &lastAutoanalyze, &vacuumCount, &autovacuumCount, &analyzeCount, &autoanalyzeCount, &totalSize); err != nil { + if err := rows.Scan(&datname, &schemaname, &relname, &seqScan, &seqTupRead, &idxScan, &idxTupFetch, &nTupIns, &nTupUpd, &nTupDel, &nTupHotUpd, &nLiveTup, &nDeadTup, &nModSinceAnalyze, &lastVacuum, &lastAutovacuum, &lastAnalyze, &lastAutoanalyze, &vacuumCount, &autovacuumCount, &analyzeCount, &autoanalyzeCount, &totalSize, &totalVacuumTime, &totalAutovacuumTime, &totalAnalyzeTime, &totalAutoanalyzeTime); err != nil { return err } @@ -437,6 +505,37 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan totalSizeMetric, datnameLabel, schemanameLabel, relnameLabel, ) + + if after18 { + // PostgreSQL 18+ vacuum/analyze timing metrics + ch <- prometheus.MustNewConstMetric( + statUserTablesTotalVacuumTime, + prometheus.CounterValue, + totalVacuumTime.Float64, + datnameLabel, schemanameLabel, relnameLabel, + ) + + ch <- prometheus.MustNewConstMetric( + statUserTablesTotalAutovacuumTime, + prometheus.CounterValue, + totalAutovacuumTime.Float64, + datnameLabel, schemanameLabel, relnameLabel, + ) + + ch <- prometheus.MustNewConstMetric( + statUserTablesTotalAnalyzeTime, + prometheus.CounterValue, + totalAnalyzeTime.Float64, + datnameLabel, schemanameLabel, relnameLabel, + ) + + ch <- prometheus.MustNewConstMetric( + statUserTablesTotalAutoanalyzeTime, + prometheus.CounterValue, + totalAutoanalyzeTime.Float64, + datnameLabel, schemanameLabel, relnameLabel, + ) + } } if err := rows.Err(); err != nil { diff --git a/collector/pg_stat_user_tables_test.go b/collector/pg_stat_user_tables_test.go index 5e82335c3..cc84469cb 100644 --- a/collector/pg_stat_user_tables_test.go +++ b/collector/pg_stat_user_tables_test.go @@ -30,7 +30,7 @@ func TestPGStatUserTablesCollector(t *testing.T) { } defer db.Close() - inst := &instance{db: db} + inst := &instance{db: db, version: pg18} lastVacuumTime, err := time.Parse("2006-01-02Z", "2023-06-02Z") if err != nil { @@ -72,7 +72,12 @@ func TestPGStatUserTablesCollector(t *testing.T) { "autovacuum_count", "analyze_count", "autoanalyze_count", - "total_size"} + "total_size", + "total_vacuum_time", + "total_autovacuum_time", + "total_analyze_time", + "total_autoanalyze_time", + } rows := sqlmock.NewRows(columns). AddRow("postgres", "public", @@ -96,8 +101,13 @@ func TestPGStatUserTablesCollector(t *testing.T) { 12, 13, 14, - 15) - mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows) + 15, + 16, + 17, + 18, + 19, + ) + mock.ExpectQuery(sanitizeQuery(statUserTablesQueryPG18)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { defer close(ch) @@ -128,6 +138,11 @@ func TestPGStatUserTablesCollector(t *testing.T) { {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 12}, {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 13}, {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 14}, + {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 15}, + {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 16}, + {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 17}, + {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 18}, + {labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 19}, } convey.Convey("Metrics comparison", t, func() { @@ -148,7 +163,7 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) { } defer db.Close() - inst := &instance{db: db} + inst := &instance{db: db, version: pg18} columns := []string{ "datname", @@ -173,7 +188,12 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) { "autovacuum_count", "analyze_count", "autoanalyze_count", - "total_size"} + "total_size", + "total_vacuum_time", + "total_autovacuum_time", + "total_analyze_time", + "total_autoanalyze_time", + } rows := sqlmock.NewRows(columns). AddRow("postgres", nil, @@ -197,8 +217,13 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) { nil, nil, nil, - nil) - mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows) + nil, + nil, + nil, + nil, + nil, + ) + mock.ExpectQuery(sanitizeQuery(statUserTablesQueryPG18)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { defer close(ch) @@ -229,6 +254,11 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) { {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, + {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0}, + {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, + {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, + {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, + {labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0}, } convey.Convey("Metrics comparison", t, func() { diff --git a/queries-lr.yaml b/queries-lr.yaml index 64ffdf824..aa9cab872 100644 --- a/queries-lr.yaml +++ b/queries-lr.yaml @@ -22,7 +22,19 @@ pg_stat_user_tables: vacuum_count, autovacuum_count, analyze_count, - autoanalyze_count + autoanalyze_count, + CASE WHEN current_setting('server_version_num')::int >= 180000 + THEN COALESCE(total_vacuum_time, 0) + ELSE 0 END as total_vacuum_time, + CASE WHEN current_setting('server_version_num')::int >= 180000 + THEN COALESCE(total_autovacuum_time, 0) + ELSE 0 END as total_autovacuum_time, + CASE WHEN current_setting('server_version_num')::int >= 180000 + THEN COALESCE(total_analyze_time, 0) + ELSE 0 END as total_analyze_time, + CASE WHEN current_setting('server_version_num')::int >= 180000 + THEN COALESCE(total_autoanalyze_time, 0) + ELSE 0 END as total_autoanalyze_time FROM pg_stat_user_tables metrics: @@ -92,6 +104,18 @@ pg_stat_user_tables: - autoanalyze_count: usage: "COUNTER" description: "Number of times this table has been analyzed by the autovacuum daemon" + - total_vacuum_time: + usage: "COUNTER" + description: "Time spent vacuuming this table, in milliseconds" + - total_autovacuum_time: + usage: "COUNTER" + description: "Time spent auto-vacuuming this table, in milliseconds" + - total_analyze_time: + usage: "COUNTER" + description: "Time spent analyzing this table, in milliseconds" + - total_autoanalyze_time: + usage: "COUNTER" + description: "Time spent auto-analyzing this table, in milliseconds" pg_statio_user_tables: query: "SELECT current_database() datname, schemaname, relname, heap_blks_read, heap_blks_hit, idx_blks_read, idx_blks_hit, toast_blks_read, toast_blks_hit, tidx_blks_read, tidx_blks_hit FROM pg_statio_user_tables"