Database monitoring (Postgres)

Sentrinel watches Postgres the way pganalyze and Datadog do: a small collector runs next to the database, polls the pg_stat_* views, and pushes what it finds to the ingest API.

Sentrinel never connects to your database. The collector holds the credentials and lives on your side of the network, so a Sentrinel compromise is not a database compromise, and no connection string is ever stored here.


Setup

1. A read-only role

CREATE ROLE sentrinel_monitor LOGIN PASSWORD 'a-strong-password';
GRANT pg_monitor TO sentrinel_monitor;

pg_monitor covers every statement the collector runs. It exists on RDS, Aurora, Cloud SQL and Azure, where superuser is not on offer.

2. pg_stat_statements โ€” optional, but it is the good part

# postgresql.conf โ€” requires a restart
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
CREATE EXTENSION pg_stat_statements;

Without it you still get activity sampling, wait events, blocking chains, connection and cache metrics, and table statistics. You lose per-query aggregates. The collector says so at startup rather than failing โ€” needing a restart scheduled is not a reason to collect nothing in the meantime.

3. Install the collector

curl -fsSL https://sentrinel.dev/install-collector.sh | sudo bash

That installs the sentrinel-collector command, a systemd service running as its own unprivileged user, and starts it on boot. Bun is installed too if the machine does not already have it.

To configure as you install โ€” the service starts immediately:

curl -fsSL https://sentrinel.dev/install-collector.sh | sudo -E env \
  DATABASE_URL=postgres://sentrinel_monitor:pw@localhost/orders \
  SENTRINEL_URL=https://api.sentrinel.dev \
  SENTRINEL_KEY=snt_live_... \
  SENTRINEL_INSTANCE=orders-primary \
  bash

Or install first and configure after:

sentrinel-collector config set \
  DATABASE_URL=postgres://sentrinel_monitor:pw@localhost/orders \
  SENTRINEL_KEY=snt_live_... \
  SENTRINEL_INSTANCE=orders-primary

sentrinel-collector check     # does the database let us in?
sentrinel-collector start

The database registers itself on first contact and appears under Databases within seconds. SENTRINEL_KEY is an ordinary ingest key โ€” it establishes which organization owns the data, nothing more.

Re-running the installer upgrades in place and leaves your configuration alone.


The command

sentrinel-collector status              is it running, and what is it watching
sentrinel-collector start|stop|restart
sentrinel-collector logs -f             follow the service output
sentrinel-collector check               connect and report what it can see
sentrinel-collector config              show settings, secrets redacted
sentrinel-collector config set K=V      change a setting and restart
sentrinel-collector run                 run in the foreground, for debugging
sentrinel-collector uninstall           remove the service, keep the config

check is the one to reach for when a chart is empty. Without pg_monitor most of pg_stat_activity still returns rows โ€” just with the query text and wait event blanked out for every backend but your own, which looks like an idle database rather than a permissions problem:

  Postgres      16.14  (primary)
  Role          holds pg_monitor
  Activity      14 backend(s) with visible query text
  Statements    pg_stat_statements installed

Configuration lives in /etc/sentrinel/collector.env, owned 0640 root:sentrinel โ€” it holds a database password and an ingest key, and nothing else on the box needs to read it.

uninstall removes the service and the CLI but keeps that file: deleting someone's credentials is not an uninstaller's decision.


What is collected, and how often

Source Interval Powers
pg_stat_statements 10s, delta'd Query list: calls, total/mean time, rows, cache hit, WAL
pg_stat_activity 1s Wait events, long transactions, blocking chains
pg_stat_database 10s Connections, cache hit ratio, deadlocks, temp spill
pg_stat_replication 10s Replication lag
pg_stat_user_tables 5m Bloat, sequential scans, vacuum lag

Activity is sampled every second on purpose. The lock you care about is held for 200ms; at a ten-second cadence it is invisible.

pg_stat_statements counters are cumulative since the last reset, so the collector ships the delta between consecutive snapshots. The first snapshot only primes the baseline โ€” otherwise months of traffic would land as one spike. A negative delta means a reset happened, and that row is dropped rather than clamped.


Query text never leaves with literals in it

Obfuscation runs in the collector, before anything is sent:

SELECT * FROM users WHERE email = 'someone@example.com'  -- what ran
SELECT * FROM users WHERE email = ?                       -- what is stored

String literals, numbers, dollar-quoted bodies and IN-list contents are all replaced. Whitespace is collapsed and long IN lists folded, so the same query groups with itself regardless of formatting or parameter count.

This matters most for pg_stat_activity, which carries the fully substituted text of whatever is running right now โ€” pg_stat_statements has usually parameterised things already, but not for clients that interpolate rather than bind.


Reading the pages

Queries ranks by share of total database time, not by mean duration. A 4ms query called a million times outranks a 900ms query called twice, and only the "% of total" column makes that visible. The collector's own polling queries appear here too โ€” that is deliberate; you should be able to see what the observer costs.

Activity shows wait events over time, the longest-running backends, and blocking chains. A row with Lock / transactionid and a blocking PID is one transaction waiting on another.

Metrics covers connections against max_connections, cache hit ratio, rollback rate and deadlocks.

Tables lists sizes and bloat, and derives advisories from them:

Advisory Fires when
bloat โ‰ฅ20% dead tuples and >10,000 of them
missing_index >1,000 sequential scans, zero index scans, >50,000 rows
index_overhead Indexes exceed twice the table size on a table >10MB

Small tables are excluded from missing_index โ€” a sequential scan of 200 rows is the right plan.


Retention

Table Kept
db_activity_samples 14 days
db_query_stats 90 days
db_instance_metrics 90 days
db_table_stats 90 days

Activity is the highest-volume table by far โ€” one row per active backend per second โ€” which is why it expires first.