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
stat_replication: fix "slot_name does not exist" by joining pg_replication_slots (#1313)
The stat_replication collector currently fails every scrape with: pq: column "slot_name" does not exist slot_name is not a column of pg_stat_replication on any supported PostgreSQL version (verified on PG 17.10.0 Docker, PG 18 Docker, and the official PG 17 and PG 18 docs). It lives on pg_replication_slots and has to be pulled in via a JOIN on active_pid = pid. Before #1306 the same collector ran via the legacy yaml-driven exporter, which used SELECT * and silently produced an empty slot_name label when the column was missing. #1306 promoted slot_name into an explicit SQL column list, turning that silent no-op into a hard parse-time error. CI did not catch it because the unit tests use sqlmock and do not validate against the real Postgres schema. stat_replication is defaultEnabled, so every user on main after #1306 sees the collector dead on every scrape, regardless of whether they run Aurora or vanilla Postgres. Fix: LEFT JOIN pg_replication_slots s ON s.active_pid = pg_stat_replication.pid and read s.slot_name. The label is populated when a named slot is in use, empty otherwise (matching the previous yaml behavior). Since pg_replication_slots.active_pid only exists on PostgreSQL 9.5+, versions 9.2-9.4 fall back to a query without the join and leave the slot_name label empty, matching the legacy yaml behavior. Verified against live postgres:9.4, 9.6 and 17 (with a streaming standby on a named slot) in Docker. Fixes #1310. Signed-off-by: Pavel K <megativ3@gmail.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
## main / (unreleased)
|
||||
|
||||
* [BUGFIX] stat_replication: fix "slot_name does not exist" by joining `pg_replication_slots` (closes #1310)
|
||||
|
||||
## 0.20.0 / 2026-06-29
|
||||
|
||||
BREAKING CHANGES:
|
||||
@@ -44,7 +46,6 @@ its `--no-collector.*` flag.
|
||||
* [BUGFIX] Fix duplicate `pg_stat_progress_vacuum` metrics for cross-database vacuums by @steinbrueckri in https://github.com/prometheus-community/postgres_exporter/pull/1262
|
||||
* [BUGFIX] Fix `pg_settings` collection when `short_desc` is NULL by @jun-gradial in https://github.com/prometheus-community/postgres_exporter/pull/1327
|
||||
|
||||
|
||||
## 0.19.1 / 2026-02-25
|
||||
|
||||
* [CHANGE] Filter and warn about duplicates in pg_stat_statements (#998) by @Nudin in https://github.com/prometheus-community/postgres_exporter/pull/1259
|
||||
|
||||
@@ -55,15 +55,20 @@ var (
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
// slot_name is not a column of pg_stat_replication; it lives on
|
||||
// pg_replication_slots and is linked via the standby's WAL sender
|
||||
// PID. LEFT JOIN so clients that do not use a named slot still get
|
||||
// a row with an empty slot_name label.
|
||||
statReplicationQuery = `
|
||||
SELECT
|
||||
application_name,
|
||||
client_addr::text,
|
||||
state,
|
||||
slot_name,
|
||||
s.slot_name,
|
||||
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_lsn('0/0'))::float else pg_wal_lsn_diff(pg_current_wal_lsn(), pg_lsn('0/0'))::float end) AS pg_current_wal_lsn_bytes,
|
||||
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), replay_lsn)::float else pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)::float end) AS pg_wal_lsn_diff
|
||||
FROM pg_stat_replication
|
||||
LEFT JOIN pg_replication_slots s ON s.active_pid = pg_stat_replication.pid
|
||||
`
|
||||
|
||||
statReplicationQueryBefore10 = `
|
||||
@@ -71,7 +76,20 @@ var (
|
||||
application_name,
|
||||
client_addr::text,
|
||||
state,
|
||||
slot_name,
|
||||
s.slot_name,
|
||||
(case pg_is_in_recovery() when 't' then pg_xlog_location_diff(pg_last_xlog_receive_location(), replay_location)::float else pg_xlog_location_diff(pg_current_xlog_location(), replay_location)::float end) AS pg_xlog_location_diff
|
||||
FROM pg_stat_replication
|
||||
LEFT JOIN pg_replication_slots s ON s.active_pid = pg_stat_replication.pid
|
||||
`
|
||||
|
||||
// pg_replication_slots.active_pid only exists on PostgreSQL 9.5+, so
|
||||
// on older versions slot_name cannot be resolved; the label is left
|
||||
// empty (matching the legacy yaml exporter behavior).
|
||||
statReplicationQueryBefore95 = `
|
||||
SELECT
|
||||
application_name,
|
||||
client_addr::text,
|
||||
state,
|
||||
(case pg_is_in_recovery() when 't' then pg_xlog_location_diff(pg_last_xlog_receive_location(), replay_location)::float else pg_xlog_location_diff(pg_current_xlog_location(), replay_location)::float end) AS pg_xlog_location_diff
|
||||
FROM pg_stat_replication
|
||||
`
|
||||
@@ -81,8 +99,10 @@ func (PGStatReplicationCollector) Update(ctx context.Context, instance *instance
|
||||
switch {
|
||||
case instance.version.GTE(semver.MustParse("10.0.0")):
|
||||
return updateStatReplication(ctx, instance, ch)
|
||||
case instance.version.GTE(semver.MustParse("9.2.0")):
|
||||
case instance.version.GTE(semver.MustParse("9.5.0")):
|
||||
return updateStatReplicationBefore10(ctx, instance, ch)
|
||||
case instance.version.GTE(semver.MustParse("9.2.0")):
|
||||
return updateStatReplicationBefore95(ctx, instance, ch)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -153,6 +173,34 @@ func updateStatReplicationBefore10(ctx context.Context, instance *instance, ch c
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func updateStatReplicationBefore95(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx, statReplicationQueryBefore95)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var applicationName, clientAddr, state sql.NullString
|
||||
var xlogLocationDiff sql.NullFloat64
|
||||
if err := rows.Scan(&applicationName, &clientAddr, &state, &xlogLocationDiff); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if xlogLocationDiff.Valid {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statReplicationXlogLocationDiffDesc,
|
||||
prometheus.GaugeValue,
|
||||
xlogLocationDiff.Float64,
|
||||
statReplicationLabelValues(applicationName, clientAddr, state, sql.NullString{})...,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func statReplicationLabelValues(applicationName, clientAddr, state, slotName sql.NullString) []string {
|
||||
return []string{
|
||||
nullStringValue(applicationName),
|
||||
|
||||
@@ -123,6 +123,55 @@ func TestPGStatReplicationCollectorBefore10(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPGStatReplicationCollectorBefore95(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, version: semver.MustParse("9.4.0")}
|
||||
|
||||
rows := sqlmock.NewRows([]string{
|
||||
"application_name",
|
||||
"client_addr",
|
||||
"state",
|
||||
"pg_xlog_location_diff",
|
||||
}).AddRow("standby", "10.0.0.1", "streaming", 32.0)
|
||||
mock.ExpectQuery(sanitizeQuery(statReplicationQueryBefore95)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatReplicationCollector{}
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatReplicationCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{
|
||||
labels: labelMap{
|
||||
"application_name": "standby",
|
||||
"client_addr": "10.0.0.1",
|
||||
"state": "streaming",
|
||||
"slot_name": "",
|
||||
},
|
||||
value: 32,
|
||||
metricType: dto.MetricType_GAUGE,
|
||||
},
|
||||
}
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
for _, expect := range expected {
|
||||
m := readMetric(<-ch)
|
||||
convey.So(expect, convey.ShouldResemble, m)
|
||||
}
|
||||
})
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled exceptions: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPGStatReplicationCollectorNullValues(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user