pgbot

module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: Apache-2.0

README

pgbot

In-database observability for PostgreSQL. One static binary connects read-only, reads Postgres's own statistics views, and prints a findings-first health report — plus what changed since last time. No agent, no external service, no write privilege anywhere in the path.

curl -fsSL https://pgbot.dev/install | sh
pgbot inspect "postgres://pgbot_ro@host:5432/db"
connected · db.example.com · postgres 17.4 · read-only · 6h20m window

Database health: 82/100

CRITICAL
● transaction-id age 1.8B — 84% toward wraparound

WARNING
● orders queries 3.2× slower (8 → 26 ms mean)
● 3 unused indexes consume 18 GB
● connection usage reached 87%

GOOD
● cache hit ratio 99.4%
● replication healthy
● no deadlocks

Details: pgbot inspect --full   ·   Machine-readable: --json
Ask it: pgbot ask "what's wrong?"

The default report is a graded read: a health score, findings bucketed CRITICAL / WARNING / NOTE, then a GOOD list naming the healthy subsystems with their values (a tool that names what it verified reads like a colleague who looked, not an alarm). pgbot inspect --full adds a subsystem status board plus the section tables and per-finding caveats; pgbot indexes drills into zero-scan indexes; pgbot ask "…" and pgbot explain put a plain-language AI reading on top of the same findings. --json is the complete, versioned contract for agents and scripts.

$ pgbot ask "what's wrong?"

Your database is mostly healthy.

1 critical issue:
orders queries became 3.2× slower in the last 6 hours.

Likely cause:
sequential scans increased after the orders table grew 18%.

Recommended:
review an index on customer_id + created_at.

Why it's not just another stats reader: pgbot remembers. Every run writes a local baseline, so from the third run on it can tell you what changed and why it matters — a query that got slower, a table that started sequential-scanning, an index that stopped being used.

See it

pgbot inspect --full — a subsystem status board (one row per subsystem, colored ok / warn / fail), followed by the detailed section tables.

pgbot indexes — zero-scan indexes with sizes, and the caveat that matters: on a primary those scan counts are per-node, so a replica may still be using an index that looks unused here. It tells you what not to drop.

pgbot ask "why is it slow?" — a plain-language reading of the same deterministic findings. It leads with the lock contention and refuses to recommend dropping the indexes because replication is active — the caveat is carried into the advice, not lost.

Install

Method Command
Script (verifies checksum) curl -fsSL https://pgbot.dev/install | sh
Go go install github.com/pgrundev/pgbot/cmd/pgbot@latest
Docker docker run --rm ghcr.io/pgrundev/pgbot inspect "$DATABASE_URL"
Homebrew brew install pgrundev/tap/pgbot

Some security teams won't pipe curl to sh — every alternative above installs the same verified binary. Releases ship SHA256 checksums signed with cosign.

Setup — a read-only role with pg_monitor

The read-only guarantee is the role, not a flag. Create a login role that holds pg_monitor (so it can see the full statistics views) and has no write grants:

CREATE ROLE pgbot_ro LOGIN PASSWORD '...';
GRANT pg_monitor TO pgbot_ro;
GRANT CONNECT ON DATABASE yourdb TO pgbot_ro;

Without pg_monitor, a non-superuser sees only its own sessions in pg_stat_activity and can't read several views fully — pgbot detects this at connect time and tells you exactly which GRANT to run rather than silently reporting partial data.

pgbot additionally pins every session read-only (default_transaction_read_only, statement_timeout=15s, lock_timeout=2s) and wraps each query in its own BEGIN READ ONLY … COMMIT. It commits those read-only probes rather than rolling them back — a read-only transaction writes nothing either way, but a rollback would inflate the xact_rollback counter pgbot itself reports. Those are defence in depth; the role is the boundary.

Usage

pgbot inspect <connection-string>   # URL or libpq DSN, or set $DATABASE_URL
  --json                 emit the versioned, PII-free Context (the agent/script contract)
  --interval 1s          gap between the two counter samples (min 500ms)
  --no-store             don't read or write the local baseline
  --no-color             disable ANSI (also honors NO_COLOR and non-TTY)

pgbot baselines list                # what's stored locally, per database
pgbot baselines prune <fingerprint> # delete a database's snapshots
pgbot baselines export <fingerprint># dump stored snapshots as JSON

pgbot indexes <connection-string>   # zero-scan indexes + what NOT to drop
pgbot explain <connection-string>   # inspect, then have an AI explain the findings
pgbot ask "why is it slow?"         # AI answer grounded on the findings ($DATABASE_URL)
  --yes                  skip the "this sends data to Google" confirmation
pgbot mcp                           # run as an MCP server over stdio (for AI agents)
MCP — use pgbot as an agent tool

pgbot mcp speaks the Model Context Protocol on stdio, so an AI agent can call pgbot as a read-only tool. It exposes deterministic tools only — inspect (full findings as JSON) and unused_indexes — and lets the connected model do the explaining. No Gemini key involved: the agent reasons over the same findings the CLI computes.

Add it to any MCP client (Claude Desktop/Code, Cursor, …):

{
  "mcpServers": {
    "pgbot": {
      "command": "pgbot",
      "args": ["mcp"],
      "env": { "DATABASE_URL": "postgres://pgbot_ro@host:5432/db" }
    }
  }
}

With DATABASE_URL set, the agent calls inspect with no arguments; or it can pass connection_string per call to reach several databases. pgbot never writes, so there's nothing an agent can break through it.

It also exposes a diagnose prompt (a one-click "inspect and give me a prioritized diagnosis" workflow) and a pgbot://baselines resource (the databases pgbot has local history for) — so tools, prompts, and resources are all available to the agent.

explain — optional AI layer

pgbot explain runs the exact same read-only inspection, prints the deterministic report unchanged, then asks a model to explain and prioritize the findings in plain language. The findings are still computed locally in Go — the model only interprets them, it never invents them, and it's instructed to carry every caveat into any recommendation. The AI text is printed below a labeled rule (🤖 generated by … — verify before acting); if the model errors or the key is unset, the deterministic report still stands.

This is the only command that sends data off the machine — the same PII-free Context you can see with inspect --json. The key is read from $GEMINI_API_KEY (never a flag), and the model/endpoint can be overridden with $PGBOT_GEMINI_MODEL / $PGBOT_GEMINI_URL.

export GEMINI_API_KEY=…            # from Google AI Studio
pgbot explain "$DATABASE_URL"

Exit codes (for CI): 0 clean · 1 warnings · 2 critical findings · 3 connection/execution failure.

Local Docker gotcha: with a database in Docker Desktop, connect via 127.0.0.1, not localhost. localhost resolves to IPv6 (::1) first, which Docker Desktop doesn't forward, so the connect stalls for ~10s before falling back to IPv4. Managed hosts (RDS, Supabase, Neon…) aren't affected.

The baseline store lives at $XDG_STATE_HOME/pgbot/baselines.db (7 days at full resolution, hourly rollups to 90 days, 100 MB cap). It's yours — inspect and delete it with pgbot baselines.

What it collects

All from SQL — connections, cache-hit ratio, TPS and rollback ratio, WAL and IO rates, checkpoints, locks and blocking chains, replication lag, top queries (pg_stat_statements), table/index sizes, dead tuples and vacuum activity, unused and missing indexes, and non-default settings. Counters (pg_stat_database, pg_stat_wal, IO) are double-sampled to produce live rates; the rest are point-in-time reads trended against the baseline.

Every section in --json carries an exactness label — sampled, cumulative, scraped, or unavailable — so a consumer never mistakes a cumulative total for a live rate.

Version support

Collectors degrade rather than fail when a capability is absent:

Feature From Fallback
pg_stat_wal (WAL rates) PG 14 section marked unavailable
pg_stat_io (buffers written) PG 16 pg_stat_bgwriter
pg_stat_checkpointer PG 17 pg_stat_bgwriter
stats_fetch_consistency PG 15 separate per-sample transactions
pg_stat_statements extension queries section unavailable + install hint

Tested against PostgreSQL 13–18.

Managed providers

pgbot detects the platform (RDS, Aurora, Cloud SQL, Azure Flexible Server, Supabase, Neon) and prints the provider-specific steps to enable pg_stat_statements when it's missing. Supabase (:6543) and Neon (-pooler) default to a pooled endpoint, which pgbot notes without degrading its rates; Neon's scale-to-zero discards stats, which pgbot handles as a cold window. Full per-provider notes and the live-verification checklist are in docs/providers.md.

Serverless Postgres (Neon, scale-to-zero)

Scale-to-zero databases (Neon, Databricks Lakebase, and similar) discard in-memory statistics when the compute suspends — by default after ~5 minutes idle. After each wake, pg_stat_statements history, cache-hit counters and index-scan counts all start again from zero.

pgbot detects this and degrades rather than lies:

  • If the statistics were reset (or the server restarted) since the last run, the entire deltas section is suppressed with a reason — a counter going from 40M to 12k is a wake, not a −99.97% change.
  • On a cold window (younger than 15 minutes), counter-based findings — unused indexes, cache-hit, sequential-scan-heavy — are suppressed, because they'd be meaningless or actively dangerous. Gauges (blocking chains, idle-in-transaction, replication lag, invalid indexes) are valid immediately and still reported.
  • The report header states the window age plainly.

If you want continuous history, disable scale-to-zero or raise the suspend timeout so the statistics survive between runs.

Not in scope (yet)

Slice 1 is honest about its edges:

  • Host OS metrics (CPU, disk IOPS, free memory) are not reachable over a SQL connection. On managed databases they live behind the provider's own API; on your own hardware, a future agent-on-host will read them.
  • AI is optional and explain-only. pgbot explain can put a plain-language explanation on top of the findings (see above), but the findings themselves are always computed deterministically in Go — no model ever generates one. Deeper correlation (pgbot why) is still future work.
  • pgbot never writes. It recommends indexes; it doesn't create them.

Privacy

Nothing leaves the machine unless you ask for it: inspect and its --json are entirely local. The one command that makes an outbound call is pgbot explain, which sends the same PII-free Context to your configured model (and says so, with a confirmation prompt).

That Context is PII-free by construction: pg_stat_statements text is normalized ($1 placeholders), and the one raw-SQL source (pg_stat_activity for blocking chains) is scrubbed of string/numeric literals, emails, and UUIDs before it can enter the Context. Connection strings are redacted in every log, error, and output. This holds for a reader of the source, not just as a claim.

License

Apache-2.0.

Directories

Path Synopsis
cmd
pgbot command
Command pgbot — in-database observability for PostgreSQL.
Command pgbot — in-database observability for PostgreSQL.
internal
ai
Package ai is pgbot's OPTIONAL explanation layer.
Package ai is pgbot's OPTIONAL explanation layer.
collect
Package collect runs the read-only diagnostic SQL and turns it into a model.Context.
Package collect runs the read-only diagnostic SQL and turns it into a model.Context.
diff
Package diff turns a current Context plus a stored baseline into model.Deltas — the "what changed and why it matters" layer that makes pgbot more than a stats reader.
Package diff turns a current Context plus a stored baseline into model.Deltas — the "what changed and why it matters" layer that makes pgbot more than a stats reader.
events
Package events derives what CHANGED in a database's schema, configuration and lifecycle between two runs — the timeline a future correlation engine needs.
Package events derives what CHANGED in a database's schema, configuration and lifecycle between two runs — the timeline a future correlation engine needs.
findings
Package findings computes deterministic, rule-based diagnoses over a model.Context.
Package findings computes deterministic, rule-based diagnoses over a model.Context.
mcp
Package mcp is a tiny Model Context Protocol server over stdio — a third renderer over pgbot's Context, for AI agents.
Package mcp is a tiny Model Context Protocol server over stdio — a third renderer over pgbot's Context, for AI agents.
model
Package model defines Context — the single public contract every pgbot surface is built on: the terminal renderer, --json, the future MCP server, and the LLM layer all consume this shape.
Package model defines Context — the single public contract every pgbot surface is built on: the terminal renderer, --json, the future MCP server, and the LLM layer all consume this shape.
rate
Package rate turns a pair of cumulative counter samples into per-second (or per-window) rates, guarding against the two ways this silently goes wrong: a counter reset between samples (negative delta) and a zero elapsed interval.
Package rate turns a pair of cumulative counter samples into per-second (or per-window) rates, guarding against the two ways this silently goes wrong: a counter reset between samples (negative delta) and a zero elapsed interval.
store
Package store is the local baseline persistence — the thing that makes pgbot temporal rather than just a stats reader.
Package store is the local baseline persistence — the thing that makes pgbot temporal rather than just a stats reader.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL