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

Add aurora_stat_database collector for Amazon Aurora PostgreSQL (#1425)

Expose where each database's block reads come from (Aurora storage,
the Optimized Reads cache or local storage) and the time spent on each,
from aurora_stat_database(), with times converted to seconds.
aurora_stat_database() also returns every pg_stat_database column; only
Aurora's own columns are exported, the rest is already covered by the
stat_database collector.

The function only exists in Aurora PostgreSQL 14.9+ and 15.4+, so the
collector checks for it first and returns no data on older versions.

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

Signed-off-by: Pavel K <megativ3@gmail.com>
This commit is contained in:
Pavel K
2026-10-21 04:51:39 +02:00
committed by GitHub
parent 53df1d5ae1
commit 26fd8c21d5
6 changed files with 326 additions and 0 deletions

View File

@@ -39,6 +39,7 @@ the `-file` flags at a mounted secret.
* [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
* [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
## 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_database`
Enable the `aurora_stat_database` collector (default: disabled).
* `[no-]collector.aurora_stat_logical_wal_cache`
Enable the `aurora_stat_logical_wal_cache` collector (default: disabled).

View File

@@ -0,0 +1,160 @@
// 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 auroraStatDatabaseSubsystem = "aurora_stat_database"
func init() {
registerCollector(auroraStatDatabaseSubsystem, serverScope, NewAuroraStatDatabaseCollector)
}
// AuroraStatDatabaseCollector exposes metrics from Amazon Aurora
// PostgreSQL's aurora_stat_database() function: where each database's block
// reads come from. aurora_stat_database() returns all pg_stat_database
// columns plus Aurora's own; only Aurora's are exported here, the rest come
// from the stat_database collector. It reports no data on servers that are
// not Aurora, or on Aurora versions without the function.
type AuroraStatDatabaseCollector struct{}
func NewAuroraStatDatabaseCollector(collectorConfig) (Collector, error) {
return &AuroraStatDatabaseCollector{}, nil
}
var (
auroraStatDatabaseStorageBlocksRead = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatDatabaseSubsystem, "storage_blocks_read_total"),
"Number of shared blocks read from Aurora storage.",
[]string{"datid", "datname"}, nil,
)
auroraStatDatabaseOrcacheBlocksHit = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatDatabaseSubsystem, "orcache_blocks_hit_total"),
"Number of blocks read from the Optimized Reads cache.",
[]string{"datid", "datname"}, nil,
)
auroraStatDatabaseLocalBlocksRead = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatDatabaseSubsystem, "local_blocks_read_total"),
"Number of local blocks read.",
[]string{"datid", "datname"}, nil,
)
auroraStatDatabaseStorageBlockReadTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatDatabaseSubsystem, "storage_block_read_time_seconds_total"),
"Time spent reading data file blocks from Aurora storage in seconds, if track_io_timing is enabled.",
[]string{"datid", "datname"}, nil,
)
auroraStatDatabaseOrcacheBlockReadTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatDatabaseSubsystem, "orcache_block_read_time_seconds_total"),
"Time spent reading data file blocks from the Optimized Reads cache in seconds, if track_io_timing is enabled.",
[]string{"datid", "datname"}, nil,
)
auroraStatDatabaseLocalBlockReadTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, auroraStatDatabaseSubsystem, "local_block_read_time_seconds_total"),
"Time spent reading local data file blocks in seconds, if track_io_timing is enabled.",
[]string{"datid", "datname"}, nil,
)
auroraStatDatabaseQuery = `SELECT
datid,
datname,
storage_blks_read,
orcache_blks_hit,
local_blks_read,
storage_blk_read_time,
orcache_blk_read_time,
local_blk_read_time
FROM aurora_stat_database()`
)
func (c AuroraStatDatabaseCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
if !instance.isAurora {
return ErrNoData
}
db := instance.getDB()
// aurora_stat_database() is only in Aurora PostgreSQL 14.9+ and 15.4+.
exists, err := functionExists(ctx, db, "aurora_stat_database")
if err != nil {
return err
}
if !exists {
return ErrNoData
}
rows, err := db.QueryContext(ctx, auroraStatDatabaseQuery)
if err != nil {
return err
}
defer rows.Close()
var found bool
for rows.Next() {
found = true
var datid, datname sql.NullString
var storageBlksRead, orcacheBlksHit, localBlksRead sql.NullInt64
var storageBlkReadTimeMsec, orcacheBlkReadTimeMsec, localBlkReadTimeMsec sql.NullFloat64
if err := rows.Scan(
&datid,
&datname,
&storageBlksRead,
&orcacheBlksHit,
&localBlksRead,
&storageBlkReadTimeMsec,
&orcacheBlkReadTimeMsec,
&localBlkReadTimeMsec,
); err != nil {
return err
}
// Like in pg_stat_database, the row for shared objects has no
// database name.
if !datid.Valid || !datname.Valid {
continue
}
labels := []string{datid.String, datname.String}
if storageBlksRead.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatDatabaseStorageBlocksRead, prometheus.CounterValue, int64CounterValue(storageBlksRead, instance.wrapLargeCounters), labels...)
}
if orcacheBlksHit.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatDatabaseOrcacheBlocksHit, prometheus.CounterValue, int64CounterValue(orcacheBlksHit, instance.wrapLargeCounters), labels...)
}
if localBlksRead.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatDatabaseLocalBlocksRead, prometheus.CounterValue, int64CounterValue(localBlksRead, instance.wrapLargeCounters), labels...)
}
if storageBlkReadTimeMsec.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatDatabaseStorageBlockReadTime, prometheus.CounterValue, storageBlkReadTimeMsec.Float64/1e3, labels...)
}
if orcacheBlkReadTimeMsec.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatDatabaseOrcacheBlockReadTime, prometheus.CounterValue, orcacheBlkReadTimeMsec.Float64/1e3, labels...)
}
if localBlkReadTimeMsec.Valid {
ch <- prometheus.MustNewConstMetric(auroraStatDatabaseLocalBlockReadTime, prometheus.CounterValue, localBlkReadTimeMsec.Float64/1e3, labels...)
}
}
if err := rows.Err(); err != nil {
return err
}
if !found {
return ErrNoData
}
return nil
}

View File

@@ -0,0 +1,159 @@
// 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 auroraStatDatabaseColumns = []string{
"datid",
"datname",
"storage_blks_read",
"orcache_blks_hit",
"local_blks_read",
"storage_blk_read_time",
"orcache_blk_read_time",
"local_blk_read_time",
}
func TestAuroraStatDatabaseCollector(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 row for shared objects has no database name and is skipped. Read
// times are exact in binary, so they convert to seconds without
// rounding errors.
expectFunctionExists(mock, "aurora_stat_database", true)
rows := sqlmock.NewRows(auroraStatDatabaseColumns).
AddRow("0", nil, 7, 0, 0, 1.5, 0, 0).
AddRow("14717", "postgres", 623, 425, 0, 3254.5, 89.5, 0)
mock.ExpectQuery(sanitizeQuery(auroraStatDatabaseQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := AuroraStatDatabaseCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling AuroraStatDatabaseCollector.Update: %s", err)
}
}()
labels := labelMap{"datid": "14717", "datname": "postgres"}
expected := []struct {
desc *prometheus.Desc
result MetricResult
}{
{auroraStatDatabaseStorageBlocksRead, MetricResult{labels: labels, value: 623, metricType: dto.MetricType_COUNTER}},
{auroraStatDatabaseOrcacheBlocksHit, MetricResult{labels: labels, value: 425, metricType: dto.MetricType_COUNTER}},
{auroraStatDatabaseLocalBlocksRead, MetricResult{labels: labels, value: 0, metricType: dto.MetricType_COUNTER}},
{auroraStatDatabaseStorageBlockReadTime, MetricResult{labels: labels, value: 3.2545, metricType: dto.MetricType_COUNTER}},
{auroraStatDatabaseOrcacheBlockReadTime, MetricResult{labels: labels, value: 0.0895, metricType: dto.MetricType_COUNTER}},
{auroraStatDatabaseLocalBlockReadTime, MetricResult{labels: labels, value: 0, 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 TestAuroraStatDatabaseCollectorNotAurora(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 := AuroraStatDatabaseCollector{}
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 TestAuroraStatDatabaseCollectorFunctionMissing(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_database", false)
ch := make(chan prometheus.Metric, 1)
c := AuroraStatDatabaseCollector{}
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 TestAuroraStatDatabaseCollectorNoRows(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_database", true)
mock.ExpectQuery(sanitizeQuery(auroraStatDatabaseQuery)).WillReturnRows(sqlmock.NewRows(auroraStatDatabaseColumns))
ch := make(chan prometheus.Metric, 1)
c := AuroraStatDatabaseCollector{}
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

@@ -75,6 +75,7 @@ const (
// Amazon Aurora PostgreSQL collectors.
CollectorAuroraGlobalDBStatus = "aurora_global_db_status"
CollectorAuroraReplicaStatus = "aurora_replica_status"
CollectorAuroraStatDatabase = "aurora_stat_database"
CollectorAuroraStatLogicalWalCache = "aurora_stat_logical_wal_cache"
)
@@ -236,6 +237,7 @@ func DefaultCollectorConfig() map[string]bool {
// Amazon Aurora PostgreSQL collectors.
CollectorAuroraGlobalDBStatus: false,
CollectorAuroraReplicaStatus: false,
CollectorAuroraStatDatabase: false,
CollectorAuroraStatLogicalWalCache: false,
}
}

View File

@@ -34,6 +34,7 @@ The long_running_transactions collector can be customized with the `--collector.
| `stat_user_tables` | enabled | Per-table: sequential vs. index scans, rows inserted/updated/deleted, live and dead tuple counts, rows modified since last analyze, last vacuum/analyze times and counts, and table and index size. How you find missing indexes and tables that aren't being vacuumed. From `pg_stat_user_tables`. |
| `statio_user_tables` | enabled | Per-table block I/O: heap, index, and TOAST blocks read from disk vs. served from shared buffers. From `pg_statio_user_tables`. |
| `statio_user_indexes` | disabled | The same read/hit split per *index* rather than per table. Off by default because it emits a series per index, which adds up quickly on large schemas. From `pg_statio_user_indexes`. |
| `aurora_stat_database` | disabled | **Amazon Aurora PostgreSQL only.** Per database: where block reads came from — Aurora storage, the Optimized Reads cache, or local storage — and the time spent on each (with `track_io_timing` on). Adds to `stat_database` rather than repeating it. Requires Aurora PostgreSQL 14.9, 15.4 or later; on older versions and on non-Aurora servers it reports nothing. From `aurora_stat_database()`. |
The per-table and per-index collectors scale with your schema — one set of series per table or index, per database scraped. On a database with thousands of tables this is the first place to look if the exporter's cardinality becomes a problem.