1
0
mirror of https://github.com/prometheus-community/postgres_exporter.git synced 2026-10-23 12:08:57 +03:00

Add aurora_stat_logical_wal_cache collector (#1414)

* Add aurora_stat_logical_wal_cache collector for Amazon Aurora PostgreSQL

Expose WAL cache hits, misses and read requests of each logical
replication slot from aurora_stat_logical_wal_cache().

The function only exists in Aurora PostgreSQL 11.17+, 12.12+, 13.8+,
14.7+ and 15.2+. Checking for Aurora alone would make the collector fail
on every scrape on older versions, so it first checks that the function
exists with to_regproc, the same way Aurora itself is detected, and
returns no data when it doesn't. The check is a small helper that other
version-dependent Aurora collectors can reuse.

The collector is disabled by default and returns no data on non-Aurora
servers.

Aurora collector names get their own block in config.go, so gofmt
does not realign the main list when longer names are added.

Signed-off-by: Pavel K <megativ3@gmail.com>

* collector: rename auroraFunctionExists to functionExists

The helper isn't Aurora specific, so it could be used by collectors
for other types of instances as well.

Signed-off-by: Pavel K <megativ3@gmail.com>

---------

Signed-off-by: Pavel K <megativ3@gmail.com>
This commit is contained in:
Pavel K
2026-10-19 04:53:36 +02:00
committed by GitHub
parent 5baef8f1b8
commit e9c27a5fb7
8 changed files with 359 additions and 4 deletions

View File

@@ -34,6 +34,7 @@ the `-file` flags at a mounted secret.
* [FEATURE] Export `pg_settings.pending_restart` as `pg_settings_pending_restart{name}` from the `settings` collector by @MarcWort in https://github.com/prometheus-community/postgres_exporter/pull/1396
* [FEATURE] Add `aurora_replica_status` collector for Amazon Aurora PostgreSQL by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1311
* [FEATURE] Add `aurora_global_db_status` collector for Amazon Aurora PostgreSQL by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1410
* [FEATURE] Add `aurora_stat_logical_wal_cache` collector for Amazon Aurora PostgreSQL by @megative in https://github.com/prometheus-community/postgres_exporter/pull/1414
## 0.20.1 / 2026-07-07

View File

@@ -109,6 +109,9 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
* `[no-]collector.aurora_replica_status`
Enable the `aurora_replica_status` collector (default: disabled).
* `[no-]collector.aurora_stat_logical_wal_cache`
Enable the `aurora_stat_logical_wal_cache` collector (default: disabled).
* `[no-]collector.database`
Enable the `database` collector (default: enabled).

View File

@@ -0,0 +1,114 @@
// 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 auroraStatLogicalWalCacheSubsystem = "aurora_stat_logical_wal_cache"
func init() {
registerCollector(auroraStatLogicalWalCacheSubsystem, serverScope, NewAuroraStatLogicalWalCacheCollector)
}
// AuroraStatLogicalWalCacheCollector exposes metrics from Amazon Aurora
// PostgreSQL's aurora_stat_logical_wal_cache() function: WAL cache usage of
// each logical replication slot. It reports no data on servers that are not
// Aurora, or on Aurora versions without the function.
type AuroraStatLogicalWalCacheCollector struct{}
func NewAuroraStatLogicalWalCacheCollector(collectorConfig) (Collector, error) {
return &AuroraStatLogicalWalCacheCollector{}, nil
}
var (
auroraStatLogicalWalCacheHits = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatLogicalWalCacheSubsystem, "cache_hits_total"),
"Number of WAL cache hits for the replication slot.",
[]string{"slot_name"}, nil,
)
auroraStatLogicalWalCacheMisses = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatLogicalWalCacheSubsystem, "cache_misses_total"),
"Number of WAL cache misses for the replication slot.",
[]string{"slot_name"}, nil,
)
auroraStatLogicalWalCacheBlocksRead = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatLogicalWalCacheSubsystem, "blocks_read_total"),
"Number of WAL cache read requests for the replication slot.",
[]string{"slot_name"}, nil,
)
auroraStatLogicalWalCacheQuery = `SELECT
name,
cache_hit,
cache_miss,
blks_read
FROM aurora_stat_logical_wal_cache()`
)
func (c AuroraStatLogicalWalCacheCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
if !instance.isAurora {
return ErrNoData
}
db := instance.getDB()
// aurora_stat_logical_wal_cache() is only in Aurora PostgreSQL 11.17+,
// 12.12+, 13.8+, 14.7+ and 15.2+.
exists, err := functionExists(ctx, db, "aurora_stat_logical_wal_cache")
if err != nil {
return err
}
if !exists {
return ErrNoData
}
rows, err := db.QueryContext(ctx, auroraStatLogicalWalCacheQuery)
if err != nil {
return err
}
defer rows.Close()
var found bool
for rows.Next() {
found = true
var slotName string
var cacheHit, cacheMiss, blksRead sql.NullInt64
if err := rows.Scan(&slotName, &cacheHit, &cacheMiss, &blksRead); err != nil {
return err
}
if cacheHit.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatLogicalWalCacheHits, prometheus.CounterValue, int64CounterValue(cacheHit, instance.wrapLargeCounters), slotName)
}
if cacheMiss.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatLogicalWalCacheMisses, prometheus.CounterValue, int64CounterValue(cacheMiss, instance.wrapLargeCounters), slotName)
}
if blksRead.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatLogicalWalCacheBlocksRead, prometheus.CounterValue, int64CounterValue(blksRead, instance.wrapLargeCounters), slotName)
}
}
if err := rows.Err(); err != nil {
return err
}
if !found {
return ErrNoData
}
return nil
}

View File

@@ -0,0 +1,151 @@
// 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 auroraStatLogicalWalCacheColumns = []string{
"name",
"cache_hit",
"cache_miss",
"blks_read",
}
func TestAuroraStatLogicalWalCacheCollector(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}
expectFunctionExists(mock, "aurora_stat_logical_wal_cache", true)
rows := sqlmock.NewRows(auroraStatLogicalWalCacheColumns).
AddRow("test_slot1", 24, 0, 24).
AddRow("test_slot2", 1, 2, 3)
mock.ExpectQuery(sanitizeQuery(auroraStatLogicalWalCacheQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := AuroraStatLogicalWalCacheCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling AuroraStatLogicalWalCacheCollector.Update: %s", err)
}
}()
expected := []struct {
desc *prometheus.Desc
result MetricResult
}{
{auroraStatLogicalWalCacheHits, MetricResult{labels: labelMap{"slot_name": "test_slot1"}, value: 24, metricType: dto.MetricType_COUNTER}},
{auroraStatLogicalWalCacheMisses, MetricResult{labels: labelMap{"slot_name": "test_slot1"}, value: 0, metricType: dto.MetricType_COUNTER}},
{auroraStatLogicalWalCacheBlocksRead, MetricResult{labels: labelMap{"slot_name": "test_slot1"}, value: 24, metricType: dto.MetricType_COUNTER}},
{auroraStatLogicalWalCacheHits, MetricResult{labels: labelMap{"slot_name": "test_slot2"}, value: 1, metricType: dto.MetricType_COUNTER}},
{auroraStatLogicalWalCacheMisses, MetricResult{labels: labelMap{"slot_name": "test_slot2"}, value: 2, metricType: dto.MetricType_COUNTER}},
{auroraStatLogicalWalCacheBlocksRead, MetricResult{labels: labelMap{"slot_name": "test_slot2"}, value: 3, metricType: dto.MetricType_COUNTER}},
}
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 TestAuroraStatLogicalWalCacheCollectorNotAurora(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 := AuroraStatLogicalWalCacheCollector{}
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 TestAuroraStatLogicalWalCacheCollectorFunctionMissing(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
// Older Aurora versions don't have the function: only the existence
// check runs.
inst := &instance{db: db, isAurora: true}
expectFunctionExists(mock, "aurora_stat_logical_wal_cache", false)
ch := make(chan prometheus.Metric, 1)
c := AuroraStatLogicalWalCacheCollector{}
if err := c.Update(context.Background(), inst, ch); err != ErrNoData {
t.Errorf("Expected ErrNoData when the function is missing, got: %v", err)
}
if len(ch) != 0 {
t.Errorf("Expected no metrics when the function is missing, got %d", len(ch))
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestAuroraStatLogicalWalCacheCollectorNoRows(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}
expectFunctionExists(mock, "aurora_stat_logical_wal_cache", true)
mock.ExpectQuery(sanitizeQuery(auroraStatLogicalWalCacheQuery)).WillReturnRows(sqlmock.NewRows(auroraStatLogicalWalCacheColumns))
ch := make(chan prometheus.Metric, 1)
c := AuroraStatLogicalWalCacheCollector{}
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)
}
}

View File

@@ -0,0 +1,33 @@
// 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"
)
// functionExistsQuery uses to_regproc, which returns NULL instead of
// raising an error when the function doesn't exist.
const functionExistsQuery = `SELECT to_regproc($1) IS NOT NULL`
// functionExists reports whether a function exists on the server. Some
// functions only exist in newer versions or on some types of instances, e.g.
// Amazon Aurora PostgreSQL; their collectors check first, so other servers
// report no data instead of an error on every scrape.
func functionExists(ctx context.Context, db *sql.DB, name string) (bool, error) {
var exists bool
err := db.QueryRowContext(ctx, functionExistsQuery, name).Scan(&exists)
return exists, err
}

View File

@@ -0,0 +1,50 @@
// 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"
)
func expectFunctionExists(mock sqlmock.Sqlmock, name string, exists bool) {
mock.ExpectQuery(sanitizeQuery(functionExistsQuery)).
WithArgs(name).
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(exists))
}
func TestFunctionExists(t *testing.T) {
for _, exists := range []bool{true, false} {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
expectFunctionExists(mock, "aurora_stat_logical_wal_cache", exists)
got, err := functionExists(context.Background(), db, "aurora_stat_logical_wal_cache")
if err != nil {
t.Errorf("Error calling functionExists: %s", err)
}
if got != exists {
t.Errorf("Expected %v, got %v", exists, got)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
db.Close()
}
}

View File

@@ -73,8 +73,9 @@ const (
CollectorXlogLocation = "xlog_location"
// Amazon Aurora PostgreSQL collectors.
CollectorAuroraGlobalDBStatus = "aurora_global_db_status"
CollectorAuroraReplicaStatus = "aurora_replica_status"
CollectorAuroraGlobalDBStatus = "aurora_global_db_status"
CollectorAuroraReplicaStatus = "aurora_replica_status"
CollectorAuroraStatLogicalWalCache = "aurora_stat_logical_wal_cache"
)
type Config struct {
@@ -233,8 +234,9 @@ func DefaultCollectorConfig() map[string]bool {
CollectorXlogLocation: false,
// Amazon Aurora PostgreSQL collectors.
CollectorAuroraGlobalDBStatus: false,
CollectorAuroraReplicaStatus: false,
CollectorAuroraGlobalDBStatus: false,
CollectorAuroraReplicaStatus: false,
CollectorAuroraStatLogicalWalCache: false,
}
}

View File

@@ -58,6 +58,7 @@ The per-table and per-index collectors scale with your schema — one set of ser
| `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_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_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