BaseExporter
On this page
Overview
Why it exists
BaseExporter is the contract every observability backend implements.
Writing a new backend — OpenTelemetry, Langfuse, a custom dashboard — means
subclassing BaseExporter and implementing export(). Nothing
else in the framework needs to change: register an instance with
get_tracer().register_exporter(MyExporter()) and every span produced by
Tracer.span() or
Tracer.record_external_span()
starts flowing to it.
When to use it
Subclass it whenever you're adding a new export destination that isn't
already covered by ConsoleExporter,
JSONExporter, or
LangSmithExporter. See the
Cookbook for a complete
worked example.
Definition
class BaseExporter(ABC):
@abstractmethod
def export(self, span: "Span") -> None:
raise NotImplementedError()
def shutdown(self) -> None:
return None
An abc.ABC — attempting to instantiate a subclass that doesn't
implement export() raises TypeError at construction time, per
standard abc behavior.
Methods
export
@abstractmethod
def export(self, span: Span) -> None
Called once per finished span, from the Tracer's background drain
thread — never from the thread that opened the span (see
Architecture → Non-blocking export).
Parameters: span (Span) — the finished span. By the time this is
called, span.end_time and span.duration are set (from
Tracer.span()'s finalization) — except when the
span arrived via record_external_span()
with end_time=None, in which case duration stays None too.
Contract: implementations must not raise. Tracer._drain_loop
already wraps each call to export() in a try/except Exception and
logs failures instead of propagating them, so a broken exporter can never
break tracing for the caller or for other exporters — but that safety
net exists specifically because the contract expects exporters to be
defensive on their own too (per the docstring in the source).
Returns: None. There's no acknowledgment or retry mechanism — if
export() fails, the span is simply not delivered to that exporter (it's
still delivered to every other registered exporter, and the failure is
logged via Python's logging module by the Tracer).
shutdown
def shutdown(self) -> None
Optional to override; the base implementation is a no-op (return None).
Override it to flush buffers or close connections —
LangSmithExporter.shutdown() is the
one built-in exporter that overrides this, to flush its client.
Called by Tracer.shutdown() for every
registered exporter, once per process exit (via atexit) or whenever you
call get_tracer().shutdown() yourself. Like export(), exceptions
raised here are caught and logged by the Tracer, not propagated.
Writing your own exporter — minimal template
from zerda.observability import BaseExporter, Span
class MyExporter(BaseExporter):
def export(self, span: Span) -> None:
try:
send_to_my_backend(span.to_dict())
except Exception:
# Be defensive even though Tracer also isolates failures.
logger.exception("failed to export span %s", span.id)
def shutdown(self) -> None:
my_backend_client.flush()
See the Cookbook for a fuller example including registration and testing with a recording double (the pattern the project's own test suite uses).
Related: ConsoleExporter · JSONExporter · LangSmithExporter · Tracer · Span