Troubleshooting and runbooks¶
Common issues, then incident runbooks for production. See also Deployment, Health and monitoring, and Migrations, upgrades, backup.
Environment and dependencies¶
| Issue | Solution |
|---|---|
| Docker not found | Install Docker Desktop; verify with docker info |
make: command not found |
Install Make (brew install make on macOS) |
| Port 8080 in use | PGQUERYNARRATIVE_PORT=8081 make start-docker (or start-local) |
go mod tidy / make lint: permission denied |
The Makefile sets GOMODCACHE=$(HOME)/.gomodcache; outside Make, export GOMODCACHE=$HOME/.gomodcache |
Database¶
| Issue | Solution |
|---|---|
| PostgreSQL connection refused | Docker: make start-docker. Local: start Postgres, then make start-local |
| Role does not exist / permission denied | make db-init then make migrate; if demo.sales denied, grant SELECT to the readonly role |
/ready returns 503 "schema migration version N < required" |
The database is behind, run migrations. See Migrations, upgrades, backup |
/ready returns 503 "dirty at version N" |
A prior migration failed partway; inspect it, fix by hand, then migrate force <version> before up |
| Fresh database: "permission denied to create extension" at migration 000019 | No DATABASE_MIGRATION_USER set, see Database roles |
CONNECTION_NOT_FOUND on a connection_id you expected to work |
The id isn't configured. It fails closed rather than silently falling back, see Multiple connections |
Reports and LLM¶
| Issue | Solution |
|---|---|
| Failed to parse narrative JSON | LLM output may be truncated; ensure Ollama is running and the model is pulled |
| Report generation fails or times out | Check LLM_BASE_URL/LLM_PROVIDER/LLM_MODEL/LLM_API_KEY; Docker + host Ollama needs LLM_BASE_URL=http://host.docker.internal:11434 |
| Cloud provider call rejected before any request | LLM_ALLOW_EXTERNAL_DATA is not true |
Extension (PostgreSQL)¶
For the pqn extension, see Troubleshooting installation and setup. The rows below are for the REST-calling pgquerynarrative extension.
| Issue | Solution |
|---|---|
CREATE EXTENSION pgquerynarrative fails |
Copy the files first: make install-extension (local) or make install-extension-docker (Docker). See PostgreSQL extension |
Functions return {"status":"pending",...} |
The http extension wasn't installed before pgquerynarrative; install it, then re-run the extension's SQL |
Functions raise permission denied for function pgquerynarrative_... |
Version 1.1 withholds EXECUTE from PUBLIC. As the extension owner run SELECT pgquerynarrative_grant_access('role'); |
Functions raise PgQueryNarrative API error: 401 |
The server has auth enabled. Call SELECT pgquerynarrative_set_api_key('...') in that session first |
Alert runbooks¶
Anchors below are linked directly from deploy/prometheus/alerts.yml's
runbook annotation: keep the {#id} on each heading stable even if the
heading text changes.
HTTP 5xx spike¶
PgqnHighHTTPErrorRate: pgqn_http_errors_total / pgqn_http_requests_total > 5%
for 10 minutes. Check recent deploys first (a bad config or image, see
Rollback), then application logs for the specific failing
route and its error.
Auth failure spike¶
PgqnAuthFailureSpike: pgqn_auth_failures_total rate > 1/s for 5 minutes. Usually
a client using an expired/rotated key, or a credential-stuffing attempt. Check
app.audit_logs for the source IPs and affected principals; rotate the key if it
was legitimately compromised.
Query timeouts¶
PgqnQueryTimeoutSpike: pgqn_query_timeouts_total rate > 0.2/s for 10 minutes.
Look for a new slow query pattern (a dashboard change, a new report) or a database
under load; QUERY_TIMEOUT can be raised temporarily, but the regression
inbox is the better long-term signal.
LLM budget exhausted¶
PgqnLLMBudgetDenials: more than 5 denials in 15 minutes. A daily/monthly/per-user
budget in Configuration – LLM has been hit.
Raise the relevant LLM_*_BUDGET* variable, or wait for the window to roll over.
DB pool saturation¶
PgqnPoolSaturation: acquired/max connections > 85% for 10 minutes on any pool. See
Pool exhaustion below.
Scheduler stuck / duplicate runs¶
PgqnSchedulerFailures (any failure in 15m) or PgqnScheduleDeadLetters (any dead
letter in 30m). See Schedule runner problems below.
Webhook delivery failures / dead letter¶
PgqnWebhookFailures (> 3 in 15m) or PgqnWebhookDeadLetters (any in 30m). See
Webhook delivery problems below.
Incident runbooks¶
Database (metadata pool) unreachable¶
Symptoms: /ready 503, "connection refused" or pool errors in logs, 5xx on
/api/*.
Actions: confirm Postgres is running and reachable (network, firewall,
credentials). Compose: check the postgres service and its logs, restart if
needed. External Postgres: check the instance, connectivity and credentials. Once
it's back, /ready should return 200 without a restart.
Analytical connection unreachable¶
Symptoms: one entry in GET /ready/connections shows "ready": false; queries
against that connection_id fail while others succeed.
Actions: /ready itself stays 200, only the app metadata pool gates it, so
this can go unnoticed without checking /ready/connections directly. Confirm the
target database, credentials, and network path for that specific connection.
Schema-version mismatch after a deploy¶
Symptoms: /ready 503 citing a version below RequiredMigrationVersion.
Actions: run migrations for the deployed version (make migrate /
make migrate-docker, or your migration Job), see
Migrations, upgrades, backup.
Dirty migration¶
Symptoms: /ready 503 citing "dirty at version N".
Actions: inspect what the failed migration actually applied, fix the schema by
hand to match, then migrate force N to clear the dirty flag, then migrate up
again. Never force past a migration whose effects you haven't verified.
OIDC / JWKS problems¶
Symptoms: browser login fails at /auth/callback; API bearer JWTs are rejected.
Actions: confirm SECURITY_OIDC_ISSUER, SECURITY_OIDC_AUDIENCE,
SECURITY_OIDC_JWKS_URL (if set) and SECURITY_OIDC_REDIRECT_URL match the IdP's
registration exactly; an empty SECURITY_OIDC_REDIRECT_URL is treated as unset and
defaults to http://localhost:8080/auth/callback, which is never right for a real
deployment. See Authentication and roles.
Session problems¶
Symptoms: users are logged out unexpectedly; login succeeds but /auth/session
reports unauthenticated.
Actions: confirm SECURITY_SESSION_SECRET hasn't changed (rotating it
invalidates every existing session) and that SECURITY_SESSION_TTL matches
expectations. Under StrictMode, session cookies are Secure; a login over plain
HTTP behind a misconfigured proxy will silently fail to persist.
Encryption-key / configuration problems¶
Symptoms: the process refuses to start with a SECURITY_DATA_ENCRYPTION_KEY or
SECURITY_SESSION_SECRET error; a config error at boot in general.
Actions: the message names the exact requirement, see
Production configuration for the full checklist config.Validate()
enforces, so you can fix the actual variable rather than trial-and-error.
invalid configuration: SECURITY_API_KEYS_JSON: …: the key list is checked strictly
and any mistake stops startup (unknown field such as keyhash, no key or key_hash,
a key_hash that is not 64 hex characters, a missing or unrecognised role,
expires_at not in RFC 3339 form, an unknown scope, or an array with no key when no
other credential is set). Fix the entry the message names; the server never starts with
a key list it could only half read. See
Authentication and roles.
Audit fail-closed behavior¶
Symptoms: queries.run, reports.generate or an admin change (API keys, memberships,
connection permissions) start failing with an audit error, with the metadata database
otherwise healthy. If they fail right after an upgrade, confirm migration 000058 has run
(/ready reports the schema version): without it the database rejects those event types.
Actions: this is SECURITY_AUDIT_MODE=required doing its job: these
high-risk actions are refused rather than left unaudited when the audit write
itself fails. Fix the underlying write failure (metadata pool health, disk space);
do not switch to best_effort in production to make the symptom go away. See
Data handling.
Distributed rate-limit backend failure¶
Symptoms: a spike in pgqn_rate_limit_storage_failures_total; requests start
failing (mode closed) or start bypassing limits (mode local_fallback).
Actions: check the metadata database; the distributed limiter is
PostgreSQL-backed. SECURITY_RATE_LIMIT_FAILURE_MODE=closed (required whenever auth
is enabled) means a backend outage here also blocks legitimate requests; that is the
intended trade-off over failing open. Restore the database, or switch to
local_fallback deliberately if you accept per-instance limiting during an outage.
Schedule runner problems¶
Symptoms: PgqnSchedulerFailures or PgqnScheduleDeadLetters firing; a
schedule stops running.
Actions: GET /schedules/{id}/runs shows recent run status and errors. A lease
that a crashed replica held is recovered automatically on the next poll; a
schedule stuck for longer than a few lease intervals (5 minutes each) usually means
the query itself is failing, not the runner. Retry a specific run with
POST /schedule-runs/{run_id}/retry.
Webhook delivery problems¶
Symptoms: PgqnWebhookFailures or PgqnWebhookDeadLetters firing;
pgqn_webhook_rejections_total rising.
Actions: GET /webhook-deliveries lists attempts and failure reasons. A
rejection (as opposed to a delivery failure) usually means the destination host
isn't in SECURITY_WEBHOOK_ALLOWED_HOSTS, or resolves to a private/loopback address
the SSRF guard blocks, see Schedules and webhooks.
A delivery failure after 5 retries dead-letters; investigate the receiving endpoint.
Regression poller problems¶
Symptoms: the regression inbox stays empty despite known slow queries; or an alert never resolves.
Actions: confirm REGRESSION_POLLER_ENABLED and
SECURITY_STAT_STATEMENTS_ENABLED are both true, and that pg_stat_statements is
actually installed and tracking on the analytical database. A query needs at least
3 baseline polling intervals before it's eligible to alert at all; a poller
restarted recently will look quiet for a while by design. On a connection whose read-only
role is shared by several organizations the poller deliberately does nothing and logs
skipping connection: give each organization its own read-only credentials. See
Regressions and applied fixes.
Pool exhaustion¶
Symptoms: PgqnPoolSaturation; requests queueing or timing out acquiring a
connection.
Actions: check pgqn_pool_acquired_conns vs _max_conns per pool (GET
/metrics). Raise DATABASE_MAX_CONNECTIONS per connection, or
DATABASE_GLOBAL_MAX_CONNECTIONS for the shared budget across all pools, within
what PostgreSQL's own max_connections allows across every client of that database.
See also¶
Health and monitoring · Migrations, upgrades, backup · Configuration reference · Deployment