Per-tenant request metrics: how the Developer API analytics feed works
Developer → API Analytics answers “what did my API traffic look like” at per-endpoint, per-API-key, p50/p95-latency, and error-rate resolution — aggregates no log viewer computes. Behind it is a deliberately isolated write pipeline: a write-side hook buffers one telemetry row per request into Redis, a worker drains that queue into a dedicatedapi_request_metrics table, and five read endpoints serve the console
from Redis-cached aggregates over that table. Request Logs and Insights
are neighbours to this surface, not parts of it — the
request-logs model and
stats surface map respectively —
this page owns the metric-emitting write pipeline.
1. What the metric plugin actually does
On every completed request, a hook fires after the response is sent and builds one row in a per-process in-memory buffer. Responses do not wait for the push — emission is fire-and-forget, so response latency never depends on Redis. A timer drains the buffer into a single shared Redis list every few seconds; a worker tick then bulk-inserts those rows into theapi_request_metrics table every 30 seconds. Each row carries:
organization_id+api_key_id(attribution; null key id for dashboard session traffic),method,path_pattern,status_code,duration_ms,request_id,client_ip(the by-key panel’s “Last IP” for dashboard rows, which have no API-key record to read),provider+cost_minor— populated by deep plumbing that tags the HTTP request id when a message send resolves its provider or debits the wallet, so the same request carries its provider class name and per-request spend for the per-key spend column.
2. Which requests fire metrics, and which deliberately don’t
The plugin splits three universes of traffic on the same finished request stream:- Programmatic API traffic (api_key_id non-null) — what you called
with a
dv_key. This is the primary dataset the per-key panels and the wallboard “API Requests (24h)” tile show. - Dashboard session traffic (api_key_id null) — browser traffic via your session, metered with a null key id so it lands in the “Dashboard / session” attribution bucket instead of a named key.
- Traffic that never fires a metric — the platform health endpoints
(
/health,/ready,/metrics), the console’s own read endpoints, and first-party realtime-transport paths (presence heartbeats, SSE token mints, the two event streams). Metering self-polling or keepalive traffic would fold page-view tubes into your API numbers, so the pipeline names these explicitly instead of drawing a blanket “anything authenticated counts” line. The console’s read endpoints also exclude any historical self-observation rows from before the write-side skip shipped — belt-and-braces so a pre-skip row doesn’t leak into a fresh window either.
3. How the Developer API analytics console consumes it
Developer → API Analytics pages in five read endpoints under/api/v1/developers/api-analytics, all Redis-cached STALE-WHILE-REVALIDATE
reads over api_request_metrics — the console polls them together, so
one refresh cycle serves cards, endpoints table, and percentiles at once:
GET /developers/api-analytics/overview— totals, error rate, top endpoints, p50/p95 over a trailing window.GET /developers/api-analytics/endpoints— per-endpoint breakdown.GET /developers/api-analytics/timeseries— request volume over time.GET /developers/api-analytics/rate-limits— the 429 panels.GET /developers/api-analytics/by-key— per-key attribution: ok / error count, latency, last IP, spend.
4. How the plugin composes with its neighbours in the guard chain
The metric plugin is guard 14 of 20 in the ordered guard chain: it registers immediately after the request logger (platform access logging) and the Sentry/base-metrics plugins, and just before idempotency, the policy scan, and the error-envelope wrapper. That order matters three ways:- It sits before auth, so it must tolerate a missing context. Most requests arrive attributed; a 429/403 short-circuit recovers from the API-key cache instead. Any resolution failure counters to the pipeline-drop metrics rather than throwing into response handling — the hook always returns the request to the chain.
- It reads the same typed
request.ctxthe sibling logger/errors/logger guards read, so a rename of the context shape fails at compile time in every observability plugin at once rather than silently mis-attributing rows. - It shares the pipeline-drop/enqueued counter names with the worker
that drains the queue, so
dropped by stage,reason / enqueuedpoints at the lossy stage (Redis unavailable, queue cap, push failure, buffer overflow) instead of reporting only a warn log.
5. Adding a new skip or extending a guard
The skip list is the pipeline’s ONE naming place — don’t scatter ad-hoc exclusions inline. When a new guard or surface needs to exclude itself (a new poller, a new health path, a new transport), follow in order:- Decide whether to even register a skip. A poll that emits rows with a genuine operator intent (a widget you opened) stays metered; the skip list only names traffic emitted on a timer, a platform probe, or the metrics surface itself. The line it draws is operator intent (a page you opened) vs transport noise (a tab that exists).
- Name the path as an exported constant next to the existing skip-list constants, then check it in the pipeline’s single predicate — the pipeline exports its predicate precisely so the skip contract can be pinned without booting the API.
- Pair the write-side skip with the read-side exclusion in the api-analytics controller, which already filters historical rows from before your skip shipped so a 30d window is correct immediately. The skip and the filter MUST share the same exported constant so the two never drift.
- Extend the existing paired test for the skip contract — this pipeline’s finished skips are all pinned so a regression is caught by a failing pin rather than a misreading dashboard.
See also
- API request guard pipeline — the 20-guard chain this plugin sits in, with status codes and ordering rules.
- The stats surface map — when
to read
/api/v1/statswidget counters vs/api/v1/analyticsroll-ups vs this pipeline. - Request Logs model — the sibling surface over Cloud Logging that serves the per-request drill-down view this pipeline aggregates.