Skip to content

ADR-0001 — Adopt a typed Facade + Sink back-end with a stable, versioned Record schema

  • Status: Accepted (proposed for 1.0)
  • Date: 2026-06-26
  • Deciders: obslog maintainers
  • Spec refs: PRODUCT.md §7 (Architecture), §8 (Record), PRIN-001..010, FR-001..006
  • Supersedes:

Context

obslog must be a domain-agnostic observability SDK whose first capability is structured logging but whose data model and pipeline extend to tracing, metrics, audit, and diagnostics without breaking changes. The defining product constraint is that output must be deterministic, machine-readable evidence suitable for an external AI agent to reconstruct execution — not human prose.

We surveyed logging, structlog, loguru, OpenTelemetry, zap, slog, zerolog, Serilog, and Microsoft.Extensions.Logging (see §1 of the design dossier). Three candidate architectures were evaluated.

Candidate A — Processor pipeline (structlog-style)

An event dict flows through ordered callables; a terminal processor renders. - Rejected as the core because the loose, untyped dict contract undermines schema stability and AI determinism — the product's reason to exist. Implicit processor ordering is a footgun; it generalizes poorly to non-log signals. - Retained idea: composability — kept as a typed, explicitly-ordered processor stage inside a sink (FR-004).

Candidate B — Facade + Sink back-end (slog / OTel API-SDK / MEL)

A thin immutable Logger builds a typed, versioned Record, gates on level, and dispatches to minimal Sinks; processors/formatters/exporters compose inside sinks; a Telemetry provider owns config and the sink graph; context flows via contextvars. - Selected. It delivers schema stability (the product thesis), a minimal stable back-end contract (ecosystem), multi-signal generality, concurrency-safe immutable context, level-gated performance, and excellent testability. It mirrors every modern winner.

Candidate C — Event bus / reactive stream

Emitters publish to an in-process queue; subscribers consume asynchronously. - Rejected as the core due to concurrency/lifecycle complexity, weak synchronous durability (bad for audit), and harder causal-chain reconstruction under reordering. - Retained idea: batching/async — kept as an optional BatchingSink/async exporter, never in the core contract (FR-005).

Decision

Adopt Candidate B as the core architecture, absorbing the composability of A (typed processors inside sinks) and the batching of C (optional sink concern), all behind a stable, versioned Record schema (PRODUCT.md §8) and a facade/back-end split.

The API/SDK split is implemented as a layering discipline within one package (obslog.api vs the implementation modules) rather than two PyPI packages on day one; nothing in obslog.api imports concrete sinks. This yields the decoupling benefit and leaves room to physically split later without an API break.

Consequences

Positive - Stable, machine-readable contract enabling deterministic AI diagnosis. - Small, stable Sink/Processor/Formatter/Exporter protocols → plugin ecosystem. - Clean path to future signals (new Record subtypes through the same pipeline). - Concurrency-safe context; level-gated, pay-for-what-you-use performance. - First-class testability (MemorySink, injectable clock/ids).

Negative / costs - More upfront type design than a string-first logger. - The Record schema must be designed carefully now because it is expensive to change later — accepted deliberately (PRIN-004, SCHEMA-001).

Follow-ups - Freeze the Record JSON Schema in M0 (schemas/obslog.record.schema.json). - Align severity numbers and reserved field names with OpenTelemetry now (NFR-COMPAT-003).