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_ENDPOINTis set. Runs alongside the Prometheus reader; both see the same instruments.
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 legacysignals_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 likePID-PGR01. Low cardinality (typically < 100 per install). In hot paths where only a UUID is available at metric-emit time, we fall back toproject_id[:8]shorthand — still bounded, operators can grep for the prefix.signal_type— the §5.2 type string literally. For broadcasts (to="*"), the value isbroadcastto distinguish from targeted types with the same payload.outcome— disjoint per-event terminal state. A singlePOST /signallands in exactly one outcome bucket.winner_surface/agent_surface—claude_desktop,claude_code,codex,cursor, orother. 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_counterinstruments on a shared meter. Both aPrometheusMetricReader(writing toprometheus_client.REGISTRY) and — when configured — aPeriodicExportingMetricReader(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.
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.
