Skip to content

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 Record from 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.