QueryProxy
Documentation menu

Monitoring

Scrape QueryProxy with Prometheus, import the example Grafana dashboard, and use queryproxy:doctor and the scheduler to see problems before your users do.

Updated:

QueryProxy has three ways to tell you it is healthy: a Prometheus endpoint, an example Grafana dashboard, and the queryproxy:doctor command. The endpoint is off until you turn it on.

The /metrics endpoint

GET /metrics returns the Prometheus text format. It answers 404 until one of these is set, and then lets a scrape through when it carries the right bearer token or comes from an allowed address:

Variable Meaning
QUERYPROXY_METRICS_TOKEN Bearer token for scrapes. Use 24 or more random characters: openssl rand -hex 24. QUERYPROXY_METRICS_TOKEN_FILE reads it from a file
QUERYPROXY_METRICS_ALLOWED_IPS Comma-separated addresses or CIDR ranges that may scrape without a token

Set one or both. Anything else gets 401. The route runs outside the web session: no cookies or CSRF.

Two things decide whether a scrape is accepted besides the token:

  • The client address follows the TRUSTED_PROXIES rules. Behind a reverse proxy, the allowlist sees the proxy’s address unless the proxy is listed. Never set TRUSTED_PROXIES=*: any client could then send its own X-Forwarded-For and pass the address allowlist.
  • The Host header must be APP_URL’s host or a name in TRUSTED_HOSTS; otherwise the request is refused with 400. Scrape the name users reach.

A Prometheus scrape configuration with a token read from a file:

scrape_configs:
  - job_name: queryproxy
    scheme: https
    metrics_path: /metrics
    authorization:
      type: Bearer
      credentials_file: /etc/prometheus/queryproxy-metrics-token
    static_configs:
      - targets: ['queryproxy.example.com']

What it exposes

Counters start at 0 for the known label sets, so rate() has a starting point.

Metric Type Labels Meaning
queryproxy_requests_total counter kind (read, write), status Requests, counted each time one enters a status. queued is not counted. Stop and cancel both count as cancelled
queryproxy_requests_pending gauge Requests waiting for a decision
queryproxy_approval_latency_seconds histogram Seconds from submission to an approve or reject decision
queryproxy_execution_duration_seconds histogram driver Seconds a request ran on its target database
queryproxy_guard_rejections_total counter rule Statements the SQL guard refused, by rule family: where_clause, privilege_management, server_control, file_io, dynamic_sql, transaction_control, session_setting, unreadable_sql, other
queryproxy_masked_values_total counter rule Result values replaced by a masking rule, by rule type: column, regex, validator
queryproxy_slot_deferred_requests_total counter Requests held back because every connection slot was busy
queryproxy_queue_depth gauge queue Jobs waiting on a queue
queryproxy_failed_jobs gauge Rows in the failed jobs table

Labels never carry SQL, user names, team names or host names. The rule label of masked values is the rule’s match type, not its name, because rule names are chosen by each team.

Notes on how the counts behave:

  • A guard rejection is counted per user action. Validating a statement and then submitting the same statement can count it twice.
  • A rejection with several rule families counts once in each family.
  • On SQLite every counted event is a database write. The table stays small because labels are bounded.
  • /metrics needs the metric_samples table. Run migrations before you start new workers.

Useful queries

# Requests waiting for a decision for a long time
queryproxy_requests_pending > 0

# 95th percentile approval latency over 1 hour
histogram_quantile(0.95, sum by (le) (rate(queryproxy_approval_latency_seconds_bucket[1h])))

# 95th percentile execution time per driver
histogram_quantile(0.95, sum by (le, driver) (rate(queryproxy_execution_duration_seconds_bucket[15m])))

# Failed requests per minute
sum(rate(queryproxy_requests_total{status="failed"}[5m])) * 60

Alert on queryproxy_failed_jobs > 0 and on a queryproxy_queue_depth that keeps growing: both mean the worker is not keeping up or not running.

The Grafana dashboard

docs/grafana-queryproxy.json in the repository is an example dashboard (uid queryproxy-overview). Import it with Grafana’s dashboard import, then pick your Prometheus data source in the Data source variable at the top. It shows pending requests, failed jobs, queue depth, requests by status and kind, approval latency (p50 and p95), execution duration by driver (p95), guard rejections, masked values and requests held for a connection slot. Treat it as a starting point; copy it and edit the copy.

The doctor

queryproxy:doctor checks configuration, database, queue, scheduler, storage, TLS and the audit chain. Run it after every deploy, and in a pipeline with --strict --json. Each check is explained in Troubleshooting.

The doctor can tell whether the scheduler runs because the scheduler writes a heartbeat to the cache every minute. A scheduler that stopped means expired results are not pruned, reminders are not sent and the daily audit checkpoint does not run.

What the scheduler runs

Job Schedule
results:prune Daily
queue:prune-failed Weekly
audit:checkpoint Daily
requests:remind Every 15 minutes
Scheduler heartbeat Every minute

Next steps

Arrow keys to move, Enter to open.