Monitor a deployment
Check health, scrape the Prometheus metrics, and set the first alerts for a self-hosted deployment.
Set this up before real consent is recorded. You watch three things: the health check, the metrics and the error lines in the log.
Before you start#
METRICS_TOKENis set, as in Deploy with Docker Compose.- You run Prometheus on a host that can reach port 3003 of the API. The console does not forward
/metrics.
Set up checks and alerts#
-
Add an HTTP check on
/health.Shell curl -s https://consent.example.in/healthA healthy API answers
200:JSON { "status": "ok", "database": "up", "redis": true, "uptimeSeconds": 8123, "timestamp": "2026-09-18T06:41:12.000Z" } -
Alert when
redisin that body isfalse. The status code stays200, but nobody can sign in. -
Write the metrics token to a file that Prometheus can read, such as
/etc/prometheus/piedap-consent.token. -
Add the scrape job.
prometheus.ymlYAML scrape_configs: - job_name: piedap-consent metrics_path: /metrics authorization: credentials_file: /etc/prometheus/piedap-consent.token static_configs: - targets: ['127.0.0.1:3003'] -
Add the first alerts.
alerts.ymlYAML groups: - name: piedap-consent rules: - alert: ConsentApiErrors expr: >- sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01 for: 10m - alert: ConsentFormLatency expr: >- histogram_quantile(0.95, sum by (le) (rate( http_request_duration_seconds_bucket{route="/sdk/v1/form"}[5m] ))) > 0.3 for: 15m - alert: ConsentWebhookFailures expr: >- sum(rate(webhook_deliveries_total{outcome!="delivered"}[15m])) > 0 for: 30m - alert: ConsentAnchorMissing expr: sum(increase(evidence_anchors_total[26h])) == 0 - alert: ConsentDashboardSlow expr: >- histogram_quantile(0.95, sum by (le) (rate( dashboard_query_seconds_bucket{cost="E"}[1d] ))) > 2 - alert: ConsentCheckSlow expr: >- histogram_quantile(0.95, sum by (le) (rate( http_request_duration_seconds_bucket{ route="/sdk/v1/consent/check"}[5m]))) > 0.3 for: 15m - alert: ConsentCheckNoRecord expr: >- sum(rate(consent_checks_total{ reason="no_record",environment="live"}[15m])) / sum(rate(consent_checks_total{environment="live"}[15m])) > 0.9 for: 30m - alert: ConsentAuditWriteFailures expr: increase(audit_log_write_failures_total[10m]) > 0 - alert: ConsentNoticeWithdrawalsPending expr: notice_withdrawals_pending > 0 for: 15m - alert: ConsentProcessorTasksOverdue expr: processor_tasks_overdue > 0 -
Reload Prometheus and check that the target
piedap-consentis up.
Use ConsentAnchorMissing only if the deployment records consent every day. A day with no sealed events has no anchor.
What to watch#
| Signal | Where | What it tells you |
|---|---|---|
/health answers 200 |
HTTP check | 503 means the database does not answer |
http_requests_total, by method, route and status |
/metrics |
The error rate |
http_request_duration_seconds, by method and route |
/metrics |
Latency. Every page view calls /sdk/v1/form |
consent_writes_total, by event, source and environment |
/metrics |
A sudden drop means sites cannot record consent |
consent_form_resolutions_total, by outcome |
/metrics |
Forms that do not resolve |
webhook_deliveries_total, by outcome |
/metrics |
rejected or failed means customers miss events |
evidence_anchors_total |
/metrics |
The daily anchor job ran |
dashboard_query_seconds, by cost |
/metrics |
Time taken by each chart query of the Overview |
dashboard_query_timeouts_total, by cost |
/metrics |
Chart queries stopped by their time limit |
consent_checks_total, by reason and environment |
/metrics |
Consent checks answered. A sudden rise of no_record can mean identifiers are hashed differently, for example after the pepper changed |
consent_import_rows_total, by mode, outcome and environment |
/metrics |
Rows checked and written by console imports |
notice_links_issued_total, by kind |
/metrics |
Notice campaign links issued |
notice_responses_total, by response |
/metrics |
Answers given on notice links |
notice_anchors_total |
/metrics |
Campaign anchors written, one for each project and day with notice answers or issued batches. A day without either adds none |
notice_withdrawals_pending |
/metrics |
Withdrawals made on a notice link over a minute ago and not yet applied to the records |
processor_tasks_overdue |
/metrics |
Live processor tasks past their due date, at the last daily check |
audit_log_write_failures_total |
/metrics |
Audit entries the database did not store. Each one is still on standard output with "stored": false |
http_request_duration_seconds has buckets at 0.2 and 0.3 seconds, besides the others. cost says what a chart query reads: R daily totals, E single events, I single records, J purposes inside records, S small tables. ConsentDashboardSlow fires when charts that read single events take more than 2 seconds over a day. Timeouts that rise in normal use mean the same. Check the database's load first, then consider a longer DASHBOARD_STATEMENT_TIMEOUT_MS.
Log lines to alert on#
Logs are JSON lines with level, message, timestamp and context. A failed job is logged with "level":"error" and raises nothing else.
| Message starts with | Meaning |
|---|---|
Daily anchor failed |
No anchors were written. The next run tries again |
Retention sweep failed, Sandbox purge failed, Expiry sweep failed |
A scheduled job stopped |
Failed to save audit log |
An audit entry was not stored. Its copy is on standard output |
Disabled webhook |
An endpoint was turned off after repeated failures |
Inactivity erasure is on for project |
The project has no endpoint for the notice, so nothing is erased |
Vendor list refresh failed |
The API cannot reach the IAB vendor list |
The last three messages are logged with "level":"warn", not "error".
Each access log line ends with the request id the caller received in X-Request-Id. Ask for that id when someone reports a failure.