Plugin & Integration Guide¶
obslog is extended through small, duck-typed protocols — no base classes, no
inheritance (PRODUCT.md §14, EXT-001). Anything that satisfies a protocol can be
dropped into the pipeline; plugins depend only on the public API and never require a
core change (EXT-003).
Extension points¶
| Protocol | Contract | Used for |
|---|---|---|
Sink |
emit(record) |
any output target |
Processor |
process(record) -> Record \| None |
enrich / redact / sample (returning None drops) |
Formatter |
format(record) -> str |
serialization at a sink's edge |
Exporter |
emit(record) + shutdown() |
delivery to external systems |
ContextExtractor |
extract() -> Mapping |
pull correlation from a framework |
Clock / IdGenerator |
time / id | deterministic testing, custom ids |
Implement one by structural typing:
class CountingSink:
def __init__(self) -> None:
self.count = 0
def emit(self, record) -> None: # matches the Sink protocol
self.count += 1
obslog.configure(sinks=[CountingSink()])
Discovery via entry points (EXT-004)¶
Third-party packages advertise components through entry points; obslog discovers them by group:
# in a plugin's pyproject.toml
[project.entry-points."obslog.sinks"]
loki = "obslog_loki:LokiSink"
from obslog.plugins import discover, Registry
sinks = discover("obslog.sinks") # {"loki": <class LokiSink>}
registry = Registry(group="obslog.sinks")
registry.load_entry_points()
Built-in integrations¶
Standard library logging (obslog.integrations, dependency-free):
from obslog.integrations import LoggingSink, ObslogHandler
obslog.configure(sinks=[LoggingSink()]) # obslog -> logging
logging.getLogger().addHandler(ObslogHandler()) # logging -> obslog
ASGI correlation (dependency-free; works with FastAPI, Starlette, …):
from obslog.integrations import ASGICorrelationMiddleware
app.add_middleware(ASGICorrelationMiddleware) # request_id/trace_id/execution_id per request
OpenTelemetry (pip install obslog[otel]):
from obslog.integrations.opentelemetry import OTelExporter
obslog.configure(
sinks=[OTelExporter()]
) # severities are already OTel-aligned, so mapping is lossless