Skip to main content

Metrics & Observability

Prism’s backend exports metrics in two ways:
  • Prometheus scrape endpoint at GET /metrics — always-on, no configuration needed. Counters + gauges become visible after first increment.
  • OTLP export to an OpenTelemetry collector — activated when OTEL_EXPORTER_OTLP_ENDPOINT is set. Runs alongside the Prometheus reader; both see the same instruments.
Every counter is labeled with pid at minimum so cardinality is bounded and per-project queries are trivial.

Endpoint

Returns Prometheus text format. No auth today (the counters are operational, not data). The path is unversioned per Prometheus convention — operators scrape one URL regardless of /api/v1 changes.
The endpoint issues a 307 redirect from /metrics to /metrics/ — standard FastAPI mount behavior. Any normal Prometheus scraper follows the redirect; manual curl users want curl -L.

Counter catalog

SPEC-030 controller plane

SPEC-030 gauges

SPEC-034 agent-to-agent signals

SPEC-101 Marconi (signal-mesh hot path + audit fan-out)

Marconi is the in-memory signal-mesh switch (SPEC-101). The legacy signals_sent_total counter remains for backward-compat; Marconi adds a parallel marconi_* namespace that disambiguates hot-path delivery from audit-pipeline durability. Every metric below MUST exist; the Stage 5 hot-path cutover required them in place before the flag flipped on server1 (2026-05-11). See Marconi for the architectural context. Hot-path counters (per tenant) Audit queue + fan-out counters Cache-invalidator counters (Stage 2) Obligation counters Unknown-recipient rejections

Label conventions

  • pid — project identifier like PID-PGR01. Low cardinality (typically < 100 per install). In hot paths where only a UUID is available at metric-emit time, we fall back to project_id[:8] shorthand — still bounded, operators can grep for the prefix.
  • signal_type — the §5.2 type string literally. For broadcasts (to="*"), the value is broadcast to distinguish from targeted types with the same payload.
  • outcome — disjoint per-event terminal state. A single POST /signal lands in exactly one outcome bucket.
  • winner_surface / agent_surfaceclaude_desktop, claude_code, codex, cursor, or other. Bounded.

Suggested Prometheus queries


Suggested alerts (baseline)

Alert thresholds are deployment-specific; these are starting points for personal / small-team installs.

Stream drops — anything > 0 is bad

Election churn — more than one election per hour per PID suggests instability

Approval timeouts — any approval timing out warrants attention

Signal drop-rate — high queued/delivered ratio

Marconi loss event — any audit-queue overwrite is a paging incident

Marconi invalidator errors — stale-route delivery risk

Marconi PG archiver lag — audit pipeline backing up

Marconi hot-path p99 latency — should be < 5ms


Scraper setup

Prometheus

Grafana Cloud Agent

curl (one-shot inspection)


Implementation notes

  • The backend uses OpenTelemetry’s Python SDK. Counters are create_counter instruments on a shared meter. Both a PrometheusMetricReader (writing to prometheus_client.REGISTRY) and — when configured — a PeriodicExportingMetricReader (OTLP gRPC) are attached. Instruments behave identically regardless of which readers are active; no export path in = silent no-op.
  • Metric emission is lazy-imported + try/except-wrapped in every service. A broken record_* call logs at DEBUG and returns — it never fails the caller. Runs counter to “fail fast” but matches the principle that observability bugs should never break the thing being observed.
  • Counter names + labels are load-bearing for dashboards. Changing them is a breaking change for anyone scraping us.
See backend/app/observability/metrics.py for the source of truth. Adding a counter is a 2-change diff: (1) new _meter.create_counter(...) at module level, (2) a record_X(...) helper to be called from services.
Last modified on May 13, 2026