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.