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).