Pipeline, Telemetry & EventBus
EventPipeline
EventPipeline routes events, spans, and usage records to multiple storage sinks with error
isolation. It is the observation backbone; live structuring and the governance stage run here
too.
pipeline = EventPipeline(sinks=[...], enricher=Enricher())
await pipeline.push(event) # also: push_span(span), push_usage(usage)
await pipeline.flush() # drain the enricher buffer, flush sinks
await pipeline.close() # flush, then close all sinks
Behavior
- Enrichment: if an enricher is configured, events pass through
enricher.process()before reaching sinks. Enricher failures fall through gracefully (the raw event still reaches sinks). - Error isolation: each sink is invoked independently, so one failing sink never blocks the others.
- Fan-out: all sinks receive every event concurrently.
- Flush: drains the enricher buffer (unpaired tool starts), then flushes all sinks.
- Close: flush + close all sinks (also error-isolated).
Telemetry / self-metrics
Two independent capabilities, neither of which pulls in a telemetry SDK:
- Export: the
OtelExporterSinksends events / spans / usage / title-updates to an OpenTelemetry collector via OTLP/HTTP JSON. It is deliberately hand-rolled (simplified OTLP JSON, noopentelemetry-sdk) to stay lightweight. - Self-metrics:
PipelineMetricsis an opt-in, in-process accumulator attached viaEventPipeline(..., metrics=PipelineMetrics()). It records throughput, enrichment latency, per-sink write time, and dropped / failed-sink counts, surfaced as an immutableMetricsSnapshotonflush()/close().
:::note The disabled path is a true no-op
Without a metrics= instance, instrumentation is fully off: no timing calls, no allocations,
and no extra dependencies or background threads. There is deliberately no opentelemetry-sdk
and no prometheus dependency.
:::
EventBus, subscribe()
An in-process consumer can react to events without implementing a full sink. The official lightweight pub/sub API lives on the pipeline:
pipeline.subscribe(on_event, *, kind=None, to_thread=False) -> CallbackSink
pipeline.unsubscribe(sink) -> bool
subscribewrapson_eventin aCallbackSink, appends it to the fan-out, and returns the sink (which doubles as the handle forunsubscribe).on_eventmay be async or a plain sync callable. Sync callbacks run inline on the event loop by default; passto_thread=Trueto run a blocking callback viaasyncio.to_thread.kindis an optional per-subscriber filter checked before dispatch: an exact kind, a"prefix.*"wildcard (e.g."tool.*"), an iterable of those, or a predicate over the event.
Because the fan-out is error-isolated, one failing subscriber never blocks the others or the
pipeline. There is intentionally no cross-process message broker: external egress is the
job of the OtelExporterSink, where OpenTelemetry is the boundary contract.