You've already forked postgres_exporter
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
114
collector/aurora_stat_logical_wal_cache.go
Normal file
114
collector/aurora_stat_logical_wal_cache.go
Normal 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
|
||||
}
|
||||
151
collector/aurora_stat_logical_wal_cache_test.go
Normal file
151
collector/aurora_stat_logical_wal_cache_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
33
collector/function_exists.go
Normal file
33
collector/function_exists.go
Normal 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
|
||||
}
|
||||
50
collector/function_exists_test.go
Normal file
50
collector/function_exists_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user