Monitoring & metrics
Scrape EvenTier with Prometheus: a token-gated metrics endpoint on the web and worker processes, queue-health gauges that page before users notice, domain counters for throughput dashboards, and a starter Grafana dashboard.
- Where:
- GET /api/metrics (web) · :9464/metrics (worker) · deploy/grafana/
- Permission:
- METRICS_TOKEN bearer token · cluster admin for Helm values
- Updated:
- July 2026
Before you start
- EvenTier exposes Prometheus text-format metrics from two places: the web process at GET /api/metrics, and the dedicated worker on port 9464 at /metrics. Both are disabled (404) until METRICS_TOKEN is set, and then require `Authorization: Bearer <token>` — generate one with `openssl rand -hex 32`.
- Database-derived gauges (queue depth, backlog age, worker heartbeat) are identical from every replica — any one target gives the full picture. Per-process series (memory, connection pool, domain counters) differ per pod, so scrape web and worker both; the Helm chart wires this for you.
- Domain counters follow standard Prometheus semantics: per-process, reset on restart or deploy. Always consume them with rate() or increase(), never raw values.
- HTTP request metrics (rate/errors/duration per route) are deliberately not exposed by the app — take them from your ingress controller (nginx-ingress and Traefik export per-service RED metrics for free), which also sees CDN and TLS behavior the app can't.
Step by step
- 01
Enable the endpoint
Set METRICS_TOKEN in your environment (or `monitoring.enabled: true` + `monitoring.metricsToken` in Helm values — the chart injects it into both processes and opens the worker's metrics port). Verify with the curl example below.
- 02
Point Prometheus at it
With the Prometheus Operator, set `monitoring.serviceMonitor.enabled: true` — one ServiceMonitor covers the web Service (/api/metrics) and the worker metrics Service (:9464/metrics). Plain Prometheus uses the scrape config example below.
- 03
Turn on the alerts
Set `monitoring.prometheusRule.enabled: true` for the three shipped rules: outbox backlog age (worker fell behind — warning at 60s), worker heartbeat stale (worker died — critical at 180s), and dead-lettered events (warning when any event exhausts its retries). Thresholds are chart values.
- 04
Import the dashboard
Import deploy/grafana/eventier-overview.json (Grafana → Dashboards → Import) and pick your Prometheus datasource. It ships queue health, ticket throughput, notification/email volume, AI token spend, memory, pool, and DB latency panels — a starting point to copy panels from into your own dashboards.
- 05
Add database-side visibility
On PostgreSQL, set `log_min_duration_statement = 500` and enable `pg_stat_statements` to catch slow queries the app-side histograms only hint at. If you run postgres_exporter, its dashboards complement this one.
Examples
curl -H "Authorization: Bearer $METRICS_TOKEN" \
https://eventier.internal.example.com/api/metrics
# Dedicated worker (in-cluster):
curl -H "Authorization: Bearer $METRICS_TOKEN" \
http://<release>-worker-metrics:9464/metricsscrape_configs:
- job_name: eventier-web
metrics_path: /api/metrics
authorization:
type: Bearer
credentials: <METRICS_TOKEN>
static_configs:
- targets: ["eventier.internal.example.com"]
scheme: https
- job_name: eventier-worker
metrics_path: /metrics
authorization:
type: Bearer
credentials: <METRICS_TOKEN>
static_configs:
- targets: ["<release>-worker-metrics:9464"]# Is the worker keeping up? (the alert signal)
max(eventier_outbox_oldest_pending_age_seconds)
# Tickets created vs resolved, per hour
sum(increase(eventier_requests_created_total[1h]))
sum(increase(eventier_requests_resolved_total[1h]))
# AI token spend by feature, per day
sum by (feature) (increase(eventier_ai_tokens_total[24h]))
# DB query latency p95 (ms)
histogram_quantile(0.95,
sum by (le) (rate(prisma_client_queries_duration_histogram_ms_bucket[5m])))Metric reference
Gauges marked DB-derived are identical on every target; counters are per-process and reset on restart — use rate()/increase(). Prisma's built-in series (prisma_*) ride along on every scrape.
| Metric | Type | Meaning |
|---|---|---|
| eventier_outbox_oldest_pending_age_seconds | Gauge (DB-derived) | Age of the oldest ready-to-claim outbox event — the "worker fell behind" signal |
| eventier_worker_last_heartbeat_age_seconds | Gauge (DB-derived) | Seconds since the worker's last heartbeat; +Inf if none ever recorded |
| eventier_outbox_events{status} | Gauge (DB-derived) | Outbox events by status: pending · processing · failed (dead letters) |
| eventier_outbox_processed_total{event_type,result} | Counter | Outbox events processed by this process, succeeded vs failed |
| eventier_requests_created_total | Counter | Tickets created — counted at the enqueue chokepoint every intake path crosses |
| eventier_requests_resolved_total | Counter | Tickets transitioned to Resolved |
| eventier_notifications_created_total{type} | Counter | In-app notification rows written |
| eventier_emails_sent_total{result} | Counter | Outbound email handoffs: sent · error |
| eventier_ai_completions_total{feature,outcome} | Counter | AI calls by feature and outcome (success · disabled · rate_limited · error) |
| eventier_ai_tokens_total{feature} | Counter | AI tokens consumed (prompt + completion) |
| process_resident_memory_bytes · nodejs_heap_* | Gauge (per-process) | Memory of each web/worker pod |
| prisma_pool_connections_* · prisma_client_queries_* | Gauge/Counter/Histogram | Prisma connection pool and query latency, per process |
Good to know
- The endpoint answers 404 — not 401 — to missing or wrong tokens, so scanners can't tell it exists. If a scrape shows 404, check the token before anything else.
- Rotating METRICS_TOKEN is safe anytime: update the secret and restart; Prometheus picks up the new credential on the next scrape.
- Watching only one number? Alert on eventier_outbox_oldest_pending_age_seconds — nearly every failure mode (worker crash, database contention, hung downstream) shows up there first.