SPEC-008 — Observability
Metrics, traces, structured logs, and the v2 event stream.
SPEC-008 — Observability
| Field | Value |
|---|---|
| Status | Draft v1 |
| Owner | kris.ganjam@gmail.com |
| Date | 2026-04-28 |
| Depends on | SPEC-001, SPEC-004 |
| Pillar | Senate |
TL;DR
Three pillars: structured logs (JSON, request_id correlation), metrics (Prometheus, per-verb counters and latencies, bucket levels), traces (OpenTelemetry, request → service → DB span). Plus the existing db_write_journal as an audit log of record. Dashboards live in Grafana with subscribe-driven panels embedded in Prism. Alerts route to a single rules file; existing fleet-health watchdog stays as the bottom-line liveness check.
1. What we have today
-
Structured-ish logs (mixed text and dict).
-
db_write_journaltable — full audit of every mutation (per-row before/after JSON, agent_id, task_id, model, slot_id). -
eventstable — pub/sub backbone, also doubles as event-log archive. -
Fleet health watchdog (Claude-powered self-healing, systemd timer every 5m). See memory entry
reference_fleet_health_watchdog.md. -
No formal metrics or traces.
2. Why now
The polymorphic substrate adds new failure modes:
-
A misbehaving validator could reject a category of writes silently.
-
A new bucket scope could throttle agents unexpectedly.
-
Embedding worker drift (semantic search) could silently degrade
mode=autoresults. -
A subscribe consumer disconnect could cause event backpressure.
Without observability, all of these are detected only by user complaint. The substrate is too important and the surface too broad.
3. Pillar 1 — Structured logs
3.1 Format
Every log line is a JSON object:
{
"ts": "2026-04-28T19:42:11.234Z",
"level": "info",
"request_id": "req-uuid",
"agent_id": "agent-uuid",
"verb": "scidex.create",
"type": "hypothesis",
"duration_ms": 87,
"outcome": "success",
"msg": "create succeeded",
"extra": { /* verb-specific */ }
}
3.2 Correlation
request_id is generated at HTTP/MCP boundary and propagated through every log line, error, event payload, and journal entry. A single failure in production is reconstructable by grep request_id.
3.3 Levels
-
error— request failed unexpectedly. Always paged. -
warn— degraded behavior (validator shadow rejection, fallback activated). -
info— verb invocations, lifecycle transitions. -
debug— verbose tracing (off by default; per-agent flag to enable).
3.4 Storage
Local rotating files; nightly ship to a long-term store (existing infra). Retention 30 days hot, 1 year cold.
4. Pillar 2 — Metrics
4.1 Counters
| Counter | Labels |
|---|---|
scidex_verb_calls_total |
verb, type, outcome |
scidex_validation_failures_total |
verb, type, validator, path_prefix |
scidex_version_conflicts_total |
type, mergeable |
scidex_rate_limit_rejections_total |
scope, kind |
scidex_events_published_total |
event_type |
scidex_events_delivered_total |
event_type |
scidex_subscribe_active_consumers |
(gauge) |
scidex_artifacts_count |
type (gauge, sampled hourly) |
4.2 Histograms
| Histogram | Labels | Buckets |
|---|---|---|
scidex_verb_duration_seconds |
verb, type | 5ms..10s |
scidex_validation_duration_seconds |
validator | 0.1ms..1s |
scidex_db_query_duration_seconds |
query_kind | 1ms..5s |
scidex_event_publish_to_deliver_seconds |
event_type | 1ms..1min |
4.3 Gauges
| Gauge | Labels |
|---|---|
scidex_rate_limit_bucket_tokens |
agent_id, scope |
scidex_quota_remaining |
agent_id, scope, period |
scidex_budget_remaining_usd |
agent_id, scope, period |
scidex_subscribe_lag_events |
consumer_id |
scidex_embed_queue_depth |
4.4 Endpoint
GET /metrics exposes Prometheus text format. Auth: substrate-internal token. Scraped by Prometheus every 30s.
5. Pillar 3 — Traces
5.1 Spans
Each verb invocation creates a root span with attributes:
verb=scidex.create
type=hypothesis
agent.id=...
agent.kind=ai_local
session.id=...
delegation_chain=[...]
request_id=...
Child spans:
-
validate.<validator_name>(per validator) -
db.write.artifacts,db.write.history,db.write.signals -
event.publish.<event_type> -
forge.dispatch.<tool_name>(when applicable)
5.2 Backend
OpenTelemetry SDK exports OTLP to a local collector → ship to Jaeger or similar (existing infra). Sampling: head-based at 10% by default; 100% for error and version_conflict.
5.3 Why traces matter for an agent-first substrate
A common debug question: “Why did this agent’s task take 30s?” Without traces, you grep logs and reconstruct a sequence. With traces, you click one trace ID and see the full call tree, including the embedding worker call, the validator that ran, the DB row-lock wait. For agent-driven workflows where one task spawns many tool calls, this matters.
6. The audit log of record: db_write_journal
Already exists. Every write captures:
-
entity_key,before_json,after_json,changed_fields -
agent_id,slot_id,model,task_id,commit_hash -
reason,source,created_at
This is not a metrics or log surface — it’s a forensic trail. It survives log rotation. Audits, dispute resolution, and post-incident reconstructions read from this.
The polymorphic verb layer extends db_write_journal with:
-
delegation_chain(full, JSONB) -
idempotency_key -
request_id(for cross-correlation)
This is the single source of truth for “what happened, who did it, when, and why.”
7. Dashboards
7.1 Grafana panels
A starter dashboard:
-
Overview — total verb calls/min, error rate, p99 latency.
-
By verb — call rate and error rate per verb.
-
By type — write rate per artifact type.
-
Validation — rejections per validator. Spike means bad agent or bad validator.
-
Locking — version_conflict rate. Spike means contention.
-
Subscribe health — active consumers, lag distribution.
-
Embed worker — queue depth, embed rate.
-
Budgets — top 10 agents by spend in the current period.
7.2 Embedded panels in Prism
Senate Quality Dashboard is in Prism, not Grafana. It subscribes to events:
-
agent.budget_exceeded— alert tile. -
system.budget_drift_detected— alert tile. -
Rolling rate of
validation_failedper validator — line chart. -
Recently superseded artifacts with reasons — feed.
The same data drives both. Grafana for ops; Prism for governance/visibility.
8. Alerts
A single alert rules file (monitoring/alerts.yaml):
| Alert | Condition | Severity |
|---|---|---|
SubstrateDown |
up{job="scidex"} == 0 for 2m |
page |
Verb5xxBurst |
rate(scidex_verb_calls_total{outcome="internal_error"}[5m]) > 0.1 |
page |
LatencyP99Regress |
histogram_quantile(0.99, scidex_verb_duration_seconds) > 1 for 10m |
warn |
ValidationStorm |
rate(scidex_validation_failures_total[5m]) > 5/s |
warn |
ConflictStorm |
rate(scidex_version_conflicts_total[5m]) > 1/s |
warn |
EmbedQueueBacklog |
scidex_embed_queue_depth > 1000 for 30m |
warn |
BudgetDrift |
system.budget_drift_detected event |
warn |
SubscribeConsumerStuck |
scidex_subscribe_lag_events > 10000 for 10m |
warn |
ActorRunaway |
single agent exceeds 90% of any budget in <1h | page (alert name retained from v1; covers any agent kind) |
Routes: page → on-call; warn → log + Senate dashboard tile.
9. Health checks
9.1 /health
-
up— process alive. -
db_reachable— can SELECT 1. -
events_writable— can INSERT to events. -
embed_worker_alive— last heartbeat within 60s. -
forge_reachable(if Forge is configured).
Returns 200 when all green; 503 with structured body listing failures otherwise.
9.2 Fleet watchdog integration
Existing orchestra-fleet-watchdog.service polls /health every 5 minutes. On red, it triggers a Claude-driven self-heal flow (see memory reference_fleet_health_watchdog.md). No change needed.
10. Privacy
Logs and metrics MUST NOT carry:
-
Passwords, JWTs, API keys, OAuth tokens.
-
Raw user input from comments/wiki content beyond a 200-char snippet.
-
Specific patient data or identifiable PII (the corpus is mostly public papers, but some artifacts are author-private — gate them).
A scrubber middleware (scidex.observability.scrub) runs on every log emission and metric label.
11. Migration plan
| # | Title | Scope | Risk |
|---|---|---|---|
| O1 | Structured-log middleware + request_id propagation |
Replace ad-hoc logging in api.py and service layer. | Low |
| O2 | Prometheus /metrics endpoint + initial counters/histograms |
New endpoint; no behavior change. | Low |
| O3 | Bucket/quota/budget gauges | Hooks into rate-limit middleware. | Low |
| O4 | OpenTelemetry tracing | SDK wiring + collector config. | Medium |
| O5 | Grafana starter dashboards | JSON dashboards committed to repo; auto-imported. | Low |
| O6 | Prism Senate Quality Dashboard panels | Frontend work; depends on Prism bootstrap. | Low (in Prism) |
| O7 | Alert rules + paging integration | Connects to existing on-call system. | Low |
| O8 | Privacy scrubber | Reviewed against current log inventory. | Medium (regression risk if it strips needed data) |
Cadence: O1-O3 in week 1. O4 week 2. O5-O7 weeks 3-4. O8 alongside.
12. Work Log
2026-05-14 — task:14978bf3 — O1-O8 migration phases shipped
O1 (Structured logging + request_id): Already complete prior to this task —
src/scidex_substrate/api/logging.py + RequestLogMiddleware.
O2 (Prometheus /metrics + domain counters/histograms): Added to metrics.py:
-
scidex_verb_calls_total{verb,type,outcome}— incremented fromapp.pyverb dispatcher -
scidex_verb_duration_seconds{verb,type}— histogram, 5ms–10s buckets, from dispatcher -
scidex_validation_failures_total{verb,type,validator,path_prefix}— stub exposed for validators to call -
scidex_version_conflicts_total{type,mergeable}— fromapp.pyonSubstrateException(version_conflict) -
scidex_rate_limit_rejections_total{scope,kind}— fromrate_limit.pyrejection branch -
scidex_events_published_total{event_type}/scidex_events_delivered_total{event_type}— stubs for event bus / SSE to call
O3 (Bucket/quota/budget gauges): Added to metrics.py:
-
scidex_subscribe_active_consumers— consumers withlast_seen_atwithin 5 min -
scidex_subscribe_lag_events{consumer_id}—max(events.seq) - last_seen_seqper consumer -
scidex_embed_queue_depth—count(embed_queue WHERE status='pending') -
scidex_rate_limit_bucket_tokens{agent_id,scope}— from in-processRateLimiter.bucket_snapshot()(agent scope only, cap 100)
O4 (OpenTelemetry tracing): New file src/scidex_substrate/api/tracing.py:
-
Feature-flagged via
SCIDEX_OBS_TRACING_ENABLED=1(default off) -
Lazy SDK init on first span; OTLP/gRPC to
SCIDEX_OTEL_ENDPOINT -
start_verb_span()/start_child_span()/record_error()public API -
Head-based sampling at
SCIDEX_OTEL_SAMPLE_RATE% (default 10%) -
Optional dep group
oteladded topyproject.toml
O5 (Grafana starter dashboard): monitoring/grafana/substrate-overview.json:
-
Panels: Overview (call rate, error rate, p50/p99 latency), By Verb, Validation, Subscribe Health, Embed Queue, Rate Limiting, Artifact counts.
O6 (Prism Senate Quality Dashboard): Prism frontend work — out of scope for this PR. Deferred to a Prism task.
O7 (Alert rules): monitoring/alerts.yaml:
-
SubstrateDown,Verb5xxBurst,LatencyP99Regress,ValidationStorm,ConflictStorm,EmbedQueueBacklog,SubscribeConsumerStuck,ActorRunaway
O8 (Privacy scrubber): Added to src/scidex_substrate/api/logging.py:
-
ScrubFilterlog filter (feature-flagged viaSCIDEX_OBS_SCRUBBER_ENABLED=1, on by default) -
Strips JWT tokens, API keys, Bearer values,
password=/secret=query params -
Truncates log field strings at 200 chars per §10 spec
2026-05-15 — task:14978bf3 — rebase + cleanup verification
Rebased branch onto origin/main (02205620), dropped unrelated collider_resolve.py and schema_registry/init.py changes from prior commit. Diff now contains exactly 9 files: spec update, 5 Python modules (logging.py, metrics.py, tracing.py, app.py, rate_limit.py), pyproject.toml (otel dep group), monitoring/alerts.yaml, and the Grafana dashboard JSON. All phases O1-O8 verified present. Python syntax check passed.
13. Open questions
-
Trace storage costs. 10% sampling at scale could still be a lot. Revisit if ingest blows up.
-
Cardinality budget for metrics.
agent_idlabel is high-cardinality (every agent). Use it sparingly; aggregate where possible. -
Audit log retention.
db_write_journalgrows linearly with writes. Plan for archival to cold storage at ~90 days. -
Prism vs Grafana split. Some dashboards are governance (Prism); some are ops (Grafana). Boundary: anything an agent or non-engineer needs to see is Prism.
-
Existing monitoring/ folder at
/home/ubuntu/monitoring— what’s there today? Likely partial; integrate with rather than replace.