Skip to content

ADR-0014: Error Model and Observability

ADR-0014: Error Model and Observability

Status: Accepted (2026-07-02) Spec: docs/superpowers/specs/2026-07-02-error-handling-observability-design.md Guide: docs/OBSERVABILITY.md

Context

Agents are the primary consumers of vinxi, over HTTP syscalls and CLI commands. Every failure must tell an agent, in machine-readable form: what happened, why (full cause chain), and how to fix it. The original VinxiError was a 9-variant enum carrying only a message string; causes were flattened into text at each adapter call site, there were no stable codes, no retry semantics, no trace correlation, and no metrics.

Decisions

  1. Struct + compile-time catalog over enriched enum or anyhow classification. VinxiError is a struct (code, message, context, cause frames, suggestions, retry override) backed by an exhaustive ErrorCode enum whose catalog() returns a complete CatalogEntry (title, description, common causes, curated suggestions, retryability, category). Fix suggestions are fundamentally a catalog problem — they must be authored once per failure mode, not improvised at ~40 call sites. A completeness test iterates all codes; shipping an undocumented error is impossible. Codes are append-only and stable.

  2. Classification at the typed boundary. Third-party errors are classified where they are still typed (classify_sqlx, classify_io). with_cause walks source() into serializable CauseFrames and intentionally does not retain the original object — downcasting after capture is impossible, which forces classification to stay at the capture site instead of degrading into string matching downstream. Consequence: nested frames carry layer label "cause" (Rust cannot name concrete types behind dyn Error); the first frame carries the real type name.

  3. RFC 9457 with same-server dereferenceable type URIs. HTTP errors are application/problem+json; type is /sys/errors/{CODE}, which the server itself serves — agents self-serve documentation. One canonical wire object (WireError) is shared by HTTP bodies, CLI stderr JSON, and worker logs; HTTP adds status/instance/request_id.

  4. No unstructured exits. anyhow is removed from all runtime paths, including binary startup. Config/bind failures use cataloged codes (VINXI_CONFIG_MISSING, VINXI_CONFIG_INVALID, VINXI_BIND_FAILED); main() returns sysexits-aligned exit codes derived from the error category; a panic hook emits one structured VINXI_PANIC event with a backtrace.

  5. Log-once rule. Errors are logged exactly once, at the boundary (HTTP handler, CLI main, worker loop, service startup). Inner layers enrich the error and return it.

  6. OTel-ready, stdout-only telemetry. vinxi-observability owns a custom JSON FormatEvent (per-line service.* resource fields, span-scope field merging, OTel semantic-convention names) and W3C TraceContext parse/generate. No OTLP exporter or backend containers yet; adding one is a subscriber-layer swap. tower-http’s TraceLayer was replaced by our own middleware, which owns the request span, the completion log line, and the traceparent/x-trace-id response headers.

  7. Trace correlation across async boundaries. The HTTP boundary overrides OperationContext.trace_id; the kernel persists it onto enqueued jobs (process.job_request.trace_id, migration 000008). error_json column exists for job-failure post-mortems and is written when the job-claim loop lands.

  8. Prometheus pull over push. metrics facade + metrics-exporter-prometheus; /metrics on server and worker. RED metrics per syscall, port-op histograms via timed_port_op, build-info gauges. vinxi_jobs_* metrics land with the job loop.

Consequences

  • Every new error code costs a catalog entry up front (enforced by test); in exchange, agents get uniform what/why/how-to-fix everywhere.
  • The wire contract (WireError, codes, exit codes, status mapping) is a public API; changes are breaking and must be versioned deliberately.
  • Kernel-core now depends on vinxi-observability (tracing + metrics facades only) — consistent with the “kernel depends on foundations and ports, never on vendors” rule.
  • Latent pedantic clippy violations were fixed workspace-wide as part of this work (lint-group priority bug had masked them).