diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d492f80..a97a697a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ the `-file` flags at a mounted secret. * [FEATURE] Add `aurora_stat_logical_wal_cache` collector for Amazon Aurora PostgreSQL by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1414 * [BUGFIX] Fix the `replication` collector failing on every scrape on Amazon Aurora PostgreSQL, where it now reports only `pg_replication_is_replica`, by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1424, based on the contribution by @dannotripp in https://github.com/prometheus-community/postgres_exporter/pull/1274 * [FEATURE] Add `aurora_stat_database` collector for Amazon Aurora PostgreSQL by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1425 +* [FEATURE] Add `aurora_global_db_instance_status` collector for Amazon Aurora PostgreSQL by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1411 ## 0.20.1 / 2026-07-07 diff --git a/README.md b/README.md index 8aeeb527..c46ac037 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,9 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra Show context-sensitive help (also try --help-long and --help-man). +* `[no-]collector.aurora_global_db_instance_status` + Enable the `aurora_global_db_instance_status` collector (default: disabled). + * `[no-]collector.aurora_global_db_status` Enable the `aurora_global_db_status` collector (default: disabled). diff --git a/collector/aurora_global_db_instance_status.go b/collector/aurora_global_db_instance_status.go new file mode 100644 index 00000000..dfd742f2 --- /dev/null +++ b/collector/aurora_global_db_instance_status.go @@ -0,0 +1,87 @@ +// Copyright 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/prometheus/client_golang/prometheus" +) + +const auroraGlobalDBInstanceStatusSubsystem = "aurora_global_db_instance_status" + +func init() { + registerCollector(auroraGlobalDBInstanceStatusSubsystem, serverScope, NewAuroraGlobalDBInstanceStatusCollector) +} + +// AuroraGlobalDBInstanceStatusCollector exposes metrics from Amazon Aurora +// PostgreSQL's aurora_global_db_instance_status() function: how far each +// instance, including the cross-Region replicas of an Aurora global +// database, lags behind the writer. It reports no data on servers that are +// not Aurora. +type AuroraGlobalDBInstanceStatusCollector struct{} + +func NewAuroraGlobalDBInstanceStatusCollector(collectorConfig) (Collector, error) { + return &AuroraGlobalDBInstanceStatusCollector{}, nil +} + +var ( + auroraGlobalDBInstanceStatusVisibilityLag = prometheus.NewDesc( + prometheus.BuildFQName(namespace, auroraGlobalDBInstanceStatusSubsystem, "visibility_lag_seconds"), + "How far the instance lags behind the writer instance in seconds.", + []string{"server_id", "aws_region"}, nil, + ) + + auroraGlobalDBInstanceStatusQuery = `SELECT + server_id, + aws_region, + visibility_lag_in_msec + FROM aurora_global_db_instance_status()` +) + +func (c AuroraGlobalDBInstanceStatusCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { + if !instance.isAurora { + return ErrNoData + } + rows, err := instance.getDB().QueryContext(ctx, auroraGlobalDBInstanceStatusQuery) + if err != nil { + return err + } + defer rows.Close() + + var found bool + for rows.Next() { + found = true + + var serverID, awsRegion string + var visibilityLagMsec sql.NullFloat64 + + if err := rows.Scan(&serverID, &awsRegion, &visibilityLagMsec); err != nil { + return err + } + + // The writer reports NULL: it doesn't lag behind itself. + if visibilityLagMsec.Valid { + ch <- prometheus.MustNewConstMetric(auroraGlobalDBInstanceStatusVisibilityLag, prometheus.GaugeValue, visibilityLagMsec.Float64/1e3, serverID, awsRegion) + } + } + if err := rows.Err(); err != nil { + return err + } + if !found { + return ErrNoData + } + return nil +} diff --git a/collector/aurora_global_db_instance_status_test.go b/collector/aurora_global_db_instance_status_test.go new file mode 100644 index 00000000..6021209c --- /dev/null +++ b/collector/aurora_global_db_instance_status_test.go @@ -0,0 +1,124 @@ +// Copyright 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/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/smartystreets/goconvey/convey" +) + +var auroraGlobalDBInstanceStatusColumns = []string{ + "server_id", + "aws_region", + "visibility_lag_in_msec", +} + +func TestAuroraGlobalDBInstanceStatusCollector(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, isAurora: true} + + // The writer reports NULL visibility lag, so nothing is emitted for it. + // The same server_id can appear in several Regions. + rows := sqlmock.NewRows(auroraGlobalDBInstanceStatusColumns). + AddRow("instance-01", "eu-west-1", nil). + AddRow("instance-02", "eu-west-1", 6). + AddRow("global-instance-1", "eu-central-1", 996). + AddRow("global-instance-1", "eu-west-2", 14) + mock.ExpectQuery(sanitizeQuery(auroraGlobalDBInstanceStatusQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := AuroraGlobalDBInstanceStatusCollector{} + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling AuroraGlobalDBInstanceStatusCollector.Update: %s", err) + } + }() + + expected := []struct { + desc *prometheus.Desc + result MetricResult + }{ + {auroraGlobalDBInstanceStatusVisibilityLag, MetricResult{labels: labelMap{"server_id": "instance-02", "aws_region": "eu-west-1"}, value: 0.006, metricType: dto.MetricType_GAUGE}}, + {auroraGlobalDBInstanceStatusVisibilityLag, MetricResult{labels: labelMap{"server_id": "global-instance-1", "aws_region": "eu-central-1"}, value: 0.996, metricType: dto.MetricType_GAUGE}}, + {auroraGlobalDBInstanceStatusVisibilityLag, MetricResult{labels: labelMap{"server_id": "global-instance-1", "aws_region": "eu-west-2"}, value: 0.014, metricType: dto.MetricType_GAUGE}}, + } + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := <-ch + convey.So(m.Desc(), convey.ShouldEqual, expect.desc) + convey.So(readMetric(m), convey.ShouldResemble, expect.result) + } + _, more := <-ch + convey.So(more, convey.ShouldBeFalse) + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} + +func TestAuroraGlobalDBInstanceStatusCollectorNotAurora(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub db connection: %s", err) + } + defer db.Close() + + // No query is expected: the collector must not touch the database when + // the server is not Aurora. + inst := &instance{db: db, isAurora: false} + + ch := make(chan prometheus.Metric, 1) + c := AuroraGlobalDBInstanceStatusCollector{} + if err := c.Update(context.Background(), inst, ch); err != ErrNoData { + t.Errorf("Expected ErrNoData on non-Aurora server, got: %v", err) + } + if len(ch) != 0 { + t.Errorf("Expected no metrics on non-Aurora server, got %d", len(ch)) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} + +func TestAuroraGlobalDBInstanceStatusCollectorNoRows(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, isAurora: true} + + mock.ExpectQuery(sanitizeQuery(auroraGlobalDBInstanceStatusQuery)).WillReturnRows(sqlmock.NewRows(auroraGlobalDBInstanceStatusColumns)) + + ch := make(chan prometheus.Metric, 1) + c := AuroraGlobalDBInstanceStatusCollector{} + if err := c.Update(context.Background(), inst, ch); err != ErrNoData { + t.Errorf("Expected ErrNoData, got: %v", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} diff --git a/config/config.go b/config/config.go index 33c45072..439346f8 100644 --- a/config/config.go +++ b/config/config.go @@ -73,10 +73,11 @@ const ( CollectorXlogLocation = "xlog_location" // Amazon Aurora PostgreSQL collectors. - CollectorAuroraGlobalDBStatus = "aurora_global_db_status" - CollectorAuroraReplicaStatus = "aurora_replica_status" - CollectorAuroraStatDatabase = "aurora_stat_database" - CollectorAuroraStatLogicalWalCache = "aurora_stat_logical_wal_cache" + CollectorAuroraGlobalDBInstanceStatus = "aurora_global_db_instance_status" + CollectorAuroraGlobalDBStatus = "aurora_global_db_status" + CollectorAuroraReplicaStatus = "aurora_replica_status" + CollectorAuroraStatDatabase = "aurora_stat_database" + CollectorAuroraStatLogicalWalCache = "aurora_stat_logical_wal_cache" ) type Config struct { @@ -235,10 +236,11 @@ func DefaultCollectorConfig() map[string]bool { CollectorXlogLocation: false, // Amazon Aurora PostgreSQL collectors. - CollectorAuroraGlobalDBStatus: false, - CollectorAuroraReplicaStatus: false, - CollectorAuroraStatDatabase: false, - CollectorAuroraStatLogicalWalCache: false, + CollectorAuroraGlobalDBInstanceStatus: false, + CollectorAuroraGlobalDBStatus: false, + CollectorAuroraReplicaStatus: false, + CollectorAuroraStatDatabase: false, + CollectorAuroraStatLogicalWalCache: false, } } diff --git a/docs/configuration.md b/docs/configuration.md index 46bc5089..22fd82f8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -64,8 +64,9 @@ and `datname` labels; update dashboards and alerts to use the names without | `wal` | enabled | Number of WAL segment files and their total size on disk, via `pg_ls_waldir()`. On Amazon Aurora PostgreSQL versions that don't support `pg_ls_waldir()`, it reports nothing. | | `stat_wal_receiver` | disabled | Seen from a standby: the WAL receiver's LSNs, timeline, upstream node, and last message send/receipt times. Enable this on replicas for a view of the receiving side. From `pg_stat_wal_receiver`. | | `xlog_location` | disabled | Current WAL write position (or replay position on a replica) as a byte offset. Superseded by `stat_replication`; retained for old setups and pre-10 servers. | -| `aurora_replica_status` | disabled | **Amazon Aurora PostgreSQL only.** For each instance in the cluster: replica lag behind the writer, replay latency, and pending page reads. On non-Aurora servers it reports nothing. From `aurora_replica_status()`. | +| `aurora_global_db_instance_status` | disabled | **Amazon Aurora PostgreSQL only.** For each instance, including the cross-Region replicas of an Aurora global database: how far it lags behind the writer, in seconds. On non-Aurora servers it reports nothing. From `aurora_global_db_instance_status()`. | | `aurora_global_db_status` | disabled | **Amazon Aurora PostgreSQL only.** For each secondary cluster of an Aurora global database, by AWS Region: storage (durability) lag and recovery point objective (RPO) lag behind the primary cluster, in seconds. On non-Aurora servers it reports nothing. From `aurora_global_db_status()`. | +| `aurora_replica_status` | disabled | **Amazon Aurora PostgreSQL only.** For each instance in the cluster: replica lag behind the writer, replay latency, and pending page reads. On non-Aurora servers it reports nothing. From `aurora_replica_status()`. | | `aurora_stat_logical_wal_cache` | disabled | **Amazon Aurora PostgreSQL only.** Per logical replication slot: WAL cache hits, misses, and read requests. Requires Aurora PostgreSQL 11.17, 12.12, 13.8, 14.7, 15.2 or later; on older versions and on non-Aurora servers it reports nothing. From `aurora_stat_logical_wal_cache()`. | ### Checkpoints, buffers, and I/O