# ADR-12: Observability hooks **Status**: accepted (2026-05-28) **Blocks**: workstream A (runtime instrumentation), workstream G (cross-cutting infra). ## Context Production users need to observe what the runtime did — which controller, which fold, which OOF refusal, which cache hit. Without structured spans, post-mortem debugging means tail-grepping log files. The bridge needs to surface these to nirs4all-studio's existing telemetry. ## Decision **`tracing` spans + JSON logfmt output + OpenTelemetry-compatible IDs + a small metrics surface.** ### Span structure The Rust runtime emits one `tracing` span per phase invocation with the following stable fields: | Field | Type | Always present? | |---|---|---| | `run_id` | UUID v7 | yes | | `plan_id` | str | yes | | `variant_id` | str | yes (after PLAN) | | `fold_id` | str | inside FIT_CV / SELECT / REFIT | | `controller_id` | str | inside controller dispatch | | `phase` | enum | yes | | `node_id` | str | inside node execution | | `partition_id` | str | inside separation-branch subgraphs (ADR-03) | | `cache_hit` | bool | when checked | | `oof_refused` | bool | on OOF leakage refusal | The span field set is **frozen**: adding fields is non-breaking, renaming or removing is governed by ADR-14. ### Sink format - Default: JSON-logfmt to stderr (one event per line, parsable by `jq`). - Optional: OpenTelemetry OTLP exporter (gated behind `--features otel`, off by default to avoid dependency bloat). - Span IDs are 128-bit, OTEL-compatible. ### Metrics surface The runtime exports a small set of counters / histograms (Prometheus-compatible names): - `dagml_phase_duration_seconds{phase}` (histogram) - `dagml_oof_refusals_total{category}` (counter) - `dagml_controller_duration_seconds{controller_id}` (histogram) - `dagml_cache_hits_total / dagml_cache_misses_total` - `dagml_lineage_records_emitted_total` ### Privacy Span fields contain **only identifiers** — never raw feature matrices, never targets, never metadata column contents. The lineage envelope (ADR-04) already enforces the same boundary at the data-layer ABI; ADR-12 extends it to the telemetry layer. Adding a non-identifier field requires an ADR-superseder and a CI lint that flags new field names matching `.*(data|features|targets|samples).*` for review. ## Consequences - Workstream A task 6 lands the `tracing` instrumentation; the C ABI exposes a `dagml_set_tracing_subscriber` hook for hosts. - Workstream G task 2 wires the field allowlist + the OTEL feature. - `nirs4all-studio`'s backend reads the JSON logfmt stream and feeds its existing telemetry widgets. ## Risk - `tracing` adds binary size; the default subscriber is lightweight (`tracing_subscriber::fmt`). The OTEL exporter is optional.