PostgreSQL¶
Monitors PostgreSQL instances — self-hosted, AWS RDS / Aurora,
Cloud SQL, Azure Flexible, Supabase. Collects health, connections,
throughput, replication, buffer cache, locks (with long-running
transaction tracking), storage with bloat estimate, backup
freshness via the WAL archiver, and aggregate pg_stat_statements
when the extension is installed.
License: Free
Prerequisites¶
- PostgreSQL 12+ (community), or the managed equivalent listed above
- Network access from the agent to the database (default port
5432) - A monitoring role with
pg_monitor— see GRANTs
Configuration¶
# probes.d/20-postgresql.yaml — each file under probes.d/ is a YAML array of probes
- name: production-postgres
type: postgresql
params:
host: db.example.com
port: 5432
username: senhub_monitor
password: ${secret:production-postgres.password} # OS secret store; inline plaintext is auto-sealed on install
database: postgres
interval: 60
timeout: 10
sslmode: verify-full
sslrootcert: /etc/ssl/db-ca.pem
Parameters¶
| Parameter | Must set | Default | Description |
|---|---|---|---|
host |
Yes | - | Server hostname or address |
port |
In practice | 5432 |
Server port |
username |
Yes | - | Monitoring role |
password |
Yes | - | Role's password. A secret: reference it with ${secret:…}, ${env:…} or ${file:…} rather than writing it in the file |
database |
No | postgres |
Database the connection opens on. Also accepted: databases |
interval |
No | 60 |
Seconds between collections |
timeout |
No | 10s |
Query timeout, seconds or a duration |
max_replication_lag_seconds |
No | 300s |
Replay lag past which a replica counts as unhealthy; 0 turns the lag term off |
sslmode |
No | - | libpq SSL mode; prefer by default. One of disable, allow, prefer, require, verify-ca, verify-full |
sslrootcert |
No | - | CA certificate path, libpq's name for tls.ca_file |
tls |
No | - | TLS settings; the block alone selects verify-full |
tls.skip_verify |
No | false |
Accept the server certificate without verifying it (selects require). Also accepted: insecure_skip_verify |
tls.ca_file |
No | - | CA certificate the server is verified against. Also accepted: ca_cert |
instance_name |
No | - | Stable identity override for this cluster |
- The probe reads cluster-wide views, so
databaseonly selects the connection. The list formdatabasesuses its first entry. sslmodeis passed to the driver as written and wins over thetlsblock. An unknown value stops the probe instead of reaching the server.- Without
sslmode, the connection negotiates TLS opportunistically (prefer). Setting atlsblock selectsverify-full; settingtls.skip_verifyselectsrequire, which is for a lab, not for production. sslrootcert,tls.ca_fileandtls.ca_certare the same setting.- Set
instance_namewhen the same server is reachable under several names, so the entity does not split.
Parameters this probe does not read¶
The paid probe this one replaced produced per-database and per-table
breakdowns. This one reports cluster-wide totals — pg_stat_database
is already summed — so three of its parameters have nothing to map onto
and are reported as errors by agent config check rather than accepted
and ignored:
| Parameter | Why |
|---|---|
expose_per_database |
there is no per-database breakdown to turn on |
expose_top_tables |
no per-table metrics are emitted |
bloat_top_n |
table bloat is not measured |
GRANTs¶
The helper prints:
CREATE ROLE senhub_monitor LOGIN PASSWORD 'STRONG-PASSWORD-HERE';
GRANT pg_monitor TO senhub_monitor;
Available since PostgreSQL 10. For PG 9.x, per-view GRANTs are required — out of scope for v1.
Optional: pg_stat_statements¶
To unlock the aggregate query metrics (senhub.db.postgres.stat_statements.*)
enable the extension:
-- as superuser
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';
-- restart the server, then:
CREATE EXTENSION pg_stat_statements;
The probe queries the right column projection automatically per server version (PG 12, 13, 17 differ on column names).
Collected Metrics¶
Every metric is tagged with metric_type so the PRTG Sensor
Builder splits them into family chips.
Every datapoint also carries the OTel resource attributes
db.system.name (postgresql), server.address, and server.port.
Overview¶
| Metric | Unit | Description |
|---|---|---|
senhub.db.up |
bool | 1 = last ping reached the server |
senhub.db.uptime.seconds |
s | now() - pg_postmaster_start_time() |
senhub.db.version.info |
– | Always 1; version carried as label |
senhub.db.connections.utilization |
ratio | count(pg_stat_activity) / max_connections |
senhub.db.replication.role |
enum | 0=standalone, 1=primary, 2=replica |
senhub.db.replication.health |
bool | Composite: WAL receiver streaming AND lag below threshold |
Connections¶
| Metric | Unit | Description |
|---|---|---|
postgresql.backends{state=active} |
count | state='active' |
postgresql.backends{state=idle} |
count | state='idle' |
postgresql.backends{state=idle_in_transaction} |
count | Vacuum killer — sessions holding open transactions while idle |
senhub.db.connections.max |
count | max_connections GUC |
Throughput¶
| Metric | Unit | Description |
|---|---|---|
senhub.db.queries.count |
counter | Commits + rollbacks across non-system DBs |
postgresql.commits |
counter | Sum of pg_stat_database.xact_commit |
postgresql.rollbacks |
counter | Sum of pg_stat_database.xact_rollback |
Replication¶
Emitted only when role is primary or replica.
| Metric | Unit | Description |
|---|---|---|
postgresql.wal.lag{operation=replay} |
s | now() - pg_last_xact_replay_timestamp() |
senhub.db.replication.io_running |
bool | 1 if pg_stat_wal_receiver.status='streaming' |
senhub.db.replication.replicas.connected |
count | count(pg_stat_replication) (primary side) |
Cache¶
| Metric | Unit | Description |
|---|---|---|
senhub.db.buffer.hit_ratio |
ratio | blks_hit / (blks_hit + blks_read) across non-system DBs |
Locks¶
| Metric | Unit | Description |
|---|---|---|
postgresql.deadlocks |
counter | Sum of pg_stat_database.deadlocks |
senhub.db.locks.waiting |
count | pg_locks where granted=false |
senhub.db.postgres.long_running_xact.seconds |
s | Age of the oldest open transaction — predicts vacuum starvation |
Storage¶
| Metric | Unit | Description |
|---|---|---|
postgresql.db_size |
bytes | Sum of pg_database_size() across non-system DBs |
postgresql.table.count |
count | pg_stat_user_tables row count |
senhub.db.postgres.bloat.ratio{schema,relation} |
ratio | Dead / (live+dead) tuples — top-N tables by heap size |
senhub.db.postgres.bloat.bytes{schema,relation} |
bytes | Wasted bytes per top-N table |
Backups¶
| Metric | Unit | Description |
|---|---|---|
senhub.db.postgres.archiver.last_archived.age.seconds |
s | DR canary — staleness of WAL archiving |
senhub.db.postgres.archiver.failed.count |
counter | Cumulative archiver failures |
Engine — pg_stat_statements (opt-in via extension)¶
| Metric | Unit | Description |
|---|---|---|
senhub.db.postgres.stat_statements.calls.count |
counter | Aggregate sum(calls) |
senhub.db.postgres.stat_statements.exec_time.mean |
s | Aggregate mean exec time across all statements (was ms in ≤0.1.91) |
Output formats¶
- PRTG / Sensor URLs tab — pick chips per family.
- Nagios — filter by family:
?tags=metric_type:replication. - Prometheus —
/api/{key}/prometheus/metrics; metric names start withsenhub_db_*andpostgresql_*. - OTLP — pushed as
senhub.db.*andpostgresql.*.
Auto-detected environments¶
environment tag set from version():
self_hosted, rds, aurora, cloudsql, azure_flexible,
supabase. Detection is best-effort; a wrong guess never breaks
the probe.
Cloud-managed instances¶
Tested against:
- AWS RDS for PostgreSQL, Aurora PostgreSQL
- GCP Cloud SQL for PostgreSQL — Insights wraps
pg_stat_statements - Azure Database for PostgreSQL Flexible Server
- Supabase
Filesystem-level metrics (WAL filesystem usage) are not available on managed instances; the WAL position and archiver age suffice.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
senhub.db.up = 0 |
Network / firewall | Verify connectivity from agent to host:port |
senhub.db.postgres.stat_statements.* missing |
Extension not installed | See the pg_stat_statements section |
senhub.db.postgres.archiver.* missing |
WAL archiving not configured | Expected — only emit when archive_mode=on and an archiver is running |
| All metrics missing | Role lacks pg_monitor |
Re-run the db-monitoring init helper |
senhub.db.postgres.bloat.* missing |
Role cannot read pg_stat_user_tables |
Same — needs pg_monitor |
Metric reference¶
Every metric this probe can emit. Metric is the OpenTelemetry name the
OTLP, Prometheus and Zabbix outputs derive theirs from. Name is what a
Nagios check and the API metrics= filter match.
PRTG channel is the label PRTG shows, placeholders filled from the
series' tags.
| Metric | Name | PRTG channel | Unit | Description |
|---|---|---|---|---|
senhub.db.up |
senhub.db.up |
Database Up | # | 1 if the agent's most recent ping reached the server, 0 otherwise |
senhub.db.postgresql.uptime |
senhub.db.postgresql.uptime |
Uptime | s | Seconds since postmaster start (now() - pg_postmaster_start_time()). Contrib postgresqlreceiver n'expose pas l'uptime — extension. |
senhub.db.version.info |
senhub.db.version.info |
Version | # | Engine version banner — value=1, version string carried in attribute db.system.version |
postgresql.backends |
postgresql.backends |
Backends ({state}) | # | Client backends grouped by connection state (active / idle / idle_in_transaction). One datapoint per state value; discriminated by the 'state' tag. |
postgresql.connection.max |
postgresql.connection.max |
Connections Max | # | max_connections GUC (instantaneous cap) |
senhub.db.connection.utilization |
senhub.db.connection.utilization |
Connections Used % | % | (active + idle + idle_in_transaction) / max_connections, percent (0-100) |
postgresql.commits |
postgresql.commits |
Transactions Committed | # | Cumulative commits across all databases (pg_stat_database.xact_commit sum) |
postgresql.rollbacks |
postgresql.rollbacks |
Transactions Rolled Back | # | Cumulative rollbacks across all databases (pg_stat_database.xact_rollback sum) |
senhub.db.postgresql.buffer.hit_ratio |
senhub.db.postgresql.buffer.hit_ratio |
Buffer Hit Ratio | % | blks_hit / (blks_hit + blks_read) over pg_stat_database. Dashboards 'santé' veulent le ratio dérivé — contrib n'expose que les compteurs bruts. |
postgresql.deadlocks |
postgresql.deadlocks |
Deadlocks | # | Cumulative deadlocks detected (pg_stat_database.deadlocks) |
senhub.db.postgresql.lock.waiting |
senhub.db.postgresql.lock.waiting |
Locks Waiting | # | Instantaneous count of granted=false rows in pg_locks |
senhub.db.postgresql.long_running_xact |
senhub.db.postgresql.long_running_xact |
Long-Running Transaction Age | s | Age in seconds of the oldest active transaction (max(now() - xact_start) over pg_stat_activity where state IN ('active','idle in transaction')) |
postgresql.db_size |
postgresql.db_size |
Database Size | B | Sum of pg_database_size() over all non-template databases |
postgresql.table.count |
postgresql.table.count |
Tables Count | # | Count of relations across user schemas (pg_class where relkind='r') |
senhub.db.postgresql.archiver.failed |
senhub.db.postgresql.archiver.failed |
Archiver Failures | # | Cumulative WAL archiver failures (pg_stat_archiver.failed_count) |
senhub.db.postgresql.archiver.last_archived.age |
senhub.db.postgresql.archiver.last_archived.age |
Last Archived WAL Age | s | Seconds since last_archived_time. Only emitted when archive_mode is enabled. |
senhub.db.replication.role |
senhub.db.replication.role |
Replication Role | # | Detected replication role of this instance |
senhub.db.replication.health |
senhub.db.replication.health |
Replication Health | # | Composite gauge: 1 if replication looks healthy (or instance is standalone), 0 if degraded |
senhub.db.replication.replicas.connected |
senhub.db.replication.replicas.connected |
Replicas Connected | # | On primary: number of streaming replicas connected (pg_stat_replication count) |
postgresql.wal.lag |
postgresql.wal.lag |
Replication Lag | s | On replica: replay lag in seconds (now() - pg_last_xact_replay_timestamp()). Attribute operation=replay (contrib canon). |
senhub.db.postgresql.replica.io.running |
senhub.db.postgresql.replica.io.running |
Replica IO Running | # | On replica: 1 if WAL receiver is connected (pg_stat_wal_receiver.status='streaming'), 0 otherwise |