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_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:
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
160
collector/aurora_stat_database.go
Normal file
160
collector/aurora_stat_database.go
Normal 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
|
||||
}
|
||||
159
collector/aurora_stat_database_test.go
Normal file
159
collector/aurora_stat_database_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user