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

Python · 7 lines
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

Python · 2 lines
@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

Python
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

Python · 12 lines
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

Related pages