Architecture¶
obslog is a typed facade + sink back-end with a stable, versioned Record schema. The full rationale and the rejected alternatives are in ADR-0001; this page summarizes the model.
The pipeline¶
user code
│ obslog.get_logger(__name__).info("order.completed", order_id=7)
▼
┌─────────────┐ builds immutable, typed ┌──────────────┐
│ Logger │ ────────────────────────────► │ Record │
│ (facade) │ (after is_enabled gate) │ (schema v1) │
└─────────────┘ └──────┬───────┘
│ resolves active │ dispatched by
▼ ▼
┌─────────────┐ ┌──────────────── Sink ───────────────────┐
│ Telemetry │ ─────► │ [Processors: enrich → redact → sample] → │
│ (provider) │ │ Formatter → write / Exporter (optional │
└─────────────┘ │ Batching) │
▲ └──────────────────────────────────────────┘
│ ambient correlation (contextvars): execution_id / trace_id / …
Roles¶
- Logger — the immutable front-end users hold. It gates on level before building
a record or evaluating lazy fields, assembles a typed
Recordfrom explicit, bound, and ambient fields (in that precedence), and dispatches it.bind()returns a new logger; the receiver is never mutated. - Record — the immutable, schema-versioned unit of evidence. Reserved fields form a
frozen contract; application data lives under
metadata/tags. The SDK carries but never interprets that data. - Telemetry (provider) — owns configuration (level, sink graph, resource, clock, id generator) and dispatches records with per-sink failure isolation. There is one explicit default provider; isolated providers are always available.
- Sink — the minimal, stable back-end contract (
emit(record)). Everything else — processors, formatters, exporters, batching — composes inside or around sinks.
Layering¶
The package encodes an API/implementation split as a discipline: obslog.api (the
facade and the extension protocols) never imports concrete sinks or formatters; the
Logger depends only on the Provider protocol (dependency inversion). This keeps the
front-end decoupled from any particular back-end.
Why this shape¶
- A stable, typed Record is the product's reason to exist — deterministic evidence an AI can consume, and a schema that can be versioned safely.
- A tiny Sink contract is what enables an ecosystem of plugins.
- The same pipeline carries future signal types (tracing, metrics, audit) as new record types — no new pipeline, no breaking change.
Correlation¶
Ambient fields (execution_id, trace_id, request_id, span_id,
parent_execution) live in contextvars, so they propagate across function calls,
threads, and async/await boundaries without manual threading. operation() scopes
create a child execution_id linked to its parent, forming a causal tree that the
diagnostic bundle later reconstructs.