Skip to content

Installing the agent

The agent is a small Node.js process that runs next to the database it observes. It reads PostgreSQL statistics read-only and pushes telemetry to the QueryPilot API. It never accepts inbound connections for data, so it works from private networks as long as it can reach the API.

  1. Open the database in the web app and find the Agents panel.
  2. Click Create agent and give it a name, for example orders-primary-agent.
  3. Copy the token. It is shown once and stored only as a SHA-256 hash. If you lose it, rotate the token.

The same can be done over the API with POST /api/v1/databases/{id}/agents; the response contains the token, the environment variables and a docker run command.

The agent is configured only through environment variables:

Variable Required Description
QUERYPILOT_API_URL Yes Base URL of the QueryPilot API, e.g. https://querypilot.internal:8080. Must be http or https.
QUERYPILOT_AGENT_TOKEN Yes The token from step 1.
QUERYPILOT_TARGET_DATABASE_URL Yes Connection string of the database to observe, using the monitoring role.
AGENT_PORT No Port of the health server. Default 8082.
LOG_LEVEL No debug, info, warn or error. Default info.

Everything else (collection intervals, statement timeout, buffer size, plan-capture settings and whether query text may be sent) is decided centrally by the API and delivered at registration and on every heartbeat.

Terminal window
docker run -d --name querypilot-agent --restart unless-stopped \
-e QUERYPILOT_API_URL='https://querypilot.internal:8080' \
-e QUERYPILOT_AGENT_TOKEN='<token>' \
-e QUERYPILOT_TARGET_DATABASE_URL='postgres://querypilot:<password>@db.internal:5432/app' \
querypilot/agent:latest

The image is built from infrastructure/docker/agent.Dockerfile (Node 22 Alpine, runs as the unprivileged node user, exposes 8082). Build it with:

Terminal window
docker build -f infrastructure/docker/agent.Dockerfile -t querypilot/agent:latest .

The repository’s docker-compose.yml has an opt-in agent service on the agent profile:

Terminal window
QUERYPILOT_AGENT_TOKEN='<token>' \
QUERYPILOT_TARGET_DATABASE_URL='postgres://querypilot:querypilot@postgres:5432/querypilot' \
docker compose --profile agent up -d agent
Terminal window
pnpm install
pnpm --filter @querypilot/agent... build
QUERYPILOT_API_URL=http://localhost:8080 \
QUERYPILOT_AGENT_TOKEN='<token>' \
QUERYPILOT_TARGET_DATABASE_URL='postgres://querypilot:<password>@localhost:5432/app' \
node apps/agent/dist/main.js
Collector Source Default interval Row cap
query-stats pg_stat_statements (top statements by total time) 15 s 1,000
activity pg_stat_activity, including wait events, application_name and the query id 5 s 500
connections pg_stat_activity 10 s None
locks pg_locks + pg_stat_activity 5 s 500
tables pg_stat_user_tables, with sizes and each table’s own autovacuum settings 60 s 1,000
indexes pg_stat_user_indexes, with size and validity 60 s 2,000
plans EXPLAIN (GENERIC_PLAN) of the busiest SELECTs 10 min 20 queries
heartbeat None 30 s None

Collectors whose capability is missing are not scheduled. Query statistics are scoped to the registered database even though pg_stat_statements is cluster-wide.

Activity samples are what Locks & waits is built from, and table and index statistics feed Tables & indexes. Sessions belonging to QueryPilot itself are excluded from activity samples, so monitoring never shows up as load.

  • Read-only sessions: default_transaction_read_only=on on every connection.
  • Two connections at most: the agent does not eat your application’s connection budget.
  • Timeouts: a server-side statement_timeout on every session (5 s by default), plus a client-side ceiling.
  • No overlap: the next run of a collector is scheduled from the end of the previous one, so a slow database produces longer gaps, not a pile-up of monitoring queries.
  • Plan capture never executes queries: see Execution plans.

The agent’s health server starts first, even when the agent is misconfigured:

Endpoint Purpose
GET /health/live Process is running.
GET /health/ready Returns checks for configuration, database (ok / unreachable) and api (ok / not registered / token rejected), plus the last error.
GET /metrics Prometheus metrics: querypilot_agent_collection_duration_ms, querypilot_agent_collection_errors_total, querypilot_agent_batches_sent_total, querypilot_agent_batches_failed_total, querypilot_agent_last_success_timestamp, querypilot_agent_queue_depth.

An agent without its required variables still starts and reports exactly what is missing on /health/ready rather than crash-looping.

Status Meaning
REGISTERED Created, but has never contacted the API.
ONLINE Heartbeating and collecting.
DEGRADED Heartbeating, but some collectors are failing or capabilities could not be read.
OFFLINE No heartbeat for 3 heartbeat intervals (90 s by default). Derived when read, so a dead agent can never look online.
REVOKED Token revoked; the agent can no longer send telemetry.
  • Rotate token issues a new token; the previous one stops working immediately. Update the agent’s QUERYPILOT_AGENT_TOKEN and restart it. While the token is rejected, the agent keeps its buffer and retries with a 60-second backoff instead of dropping data.
  • Revoke permanently disables the agent’s token.
  • If PostgreSQL or the API is unavailable at startup, the agent retries registration with backoff (1 s doubling up to 60 s).
  • During an API outage, samples are buffered in memory up to the configured row limit, oldest dropped first. The number of dropped samples is reported in the next heartbeat.
  • On SIGTERM the agent stops collecting, makes one bounded attempt (5 s) to deliver what is buffered, and disconnects.