Tracer

On this page

Overview

Why it exists

Tracer is the process-wide object that all context-manager and lifecycle behavior for spans lives on — creating them (with correct parent/trace linkage), finishing them, and dispatching finished spans to every registered exporter on a background thread. Span itself (span.py) is a passive dataclass; Tracer is what makes it a tracing system.

When to use it directly

Use get_tracer() (not Tracer()) any time you want to open a manual span, register/unregister an exporter yourself, check whether tracing is active, or force a flush before your process exits. The enable_*() helpers in __init__.py are thin convenience wrappers around get_tracer().register_exporter(...) and cover the common cases.

When not to use it

Don't call Tracer() directly — instantiating it yourself gives you a tracer with its own private exporter list, disconnected from anything the rest of the framework (chains, RAG pipelines, routers) reports to. Use get_tracer() to get the shared singleton.


Constructor

Python
Tracer() -> None

Not intended to be called by user code (see above). Initializes an empty exporter list, _enabled = False, a threading.Lock, an internal queue.Queue, no worker thread yet, and registers self.shutdown with atexit.


Properties

enabled

Python · 2 lines
@property
def enabled(self) -> bool

True only when both self._enabled is True and at least one exporter is registered (bool(self._exporters)). This is why registering your first exporter is enough to start tracing without a separate .enable() call — but it also means clearing all exporters (clear_exporters()) silently makes enabled False again even if you never called .disable().

exporters

Python · 2 lines
@property
def exporters(self) -> List[Any]

Returns a copy (list(self._exporters)) of the currently registered exporters, taken under the internal lock. Mutating the returned list does not affect the tracer's internal state.


Methods

enable

Python
def enable(self) -> None

Sets self._enabled = True and ensures the background worker thread is running (via the private _ensure_worker()). Does not register any exporters — calling enable() with zero exporters registered leaves self.enabled (the property) False, per the enabled property's logic above.

disable

Python
def disable(self) -> None

Sets self._enabled = False. Registered exporters are not cleared — a later enable() (or register_exporter()) resumes delivery to them. This is what disable_tracing() calls.

register_exporter

Python
def register_exporter(self, exporter: Any) -> None

Appends exporter to the internal list (skipping it if the exact same object, by in/__eq__, is already registered — BaseExporter subclasses don't override __eq__, so in practice this means identity), under the lock, then calls self.enable(). This is why every enable_langsmith() / enable_console() / enable_json() call — and any manual get_tracer().register_exporter(MyExporter()) — turns tracing on as a side effect.

Parameters

NameTypeRequiredNotes
exporterAnyyesExpected to implement the BaseExporter interface (export(span), optional shutdown()); not type-checked at registration time.

unregister_exporter

Python
def unregister_exporter(self, exporter: Any) -> None

Removes exporter from the internal list if present (no error if it isn't). Does not call disable() — if this was the last exporter, self._enabled stays True but the enabled property now evaluates to False (since bool(self._exporters) is False).

clear_exporters

Python
def clear_exporters(self) -> None

Empties the exporter list entirely, under the lock. Used by the test suite's _reset_tracer fixture to isolate tests from each other.

span

Python · 10 lines
@contextmanager
def span(
    self,
    name: str,
    type: str = "generic",
    input: Any = None,
    metadata: Optional[Dict[str, Any]] = None,
    model: Optional[str] = None,
    provider: Optional[str] = None,
) -> Generator[Span, None, None]

The primary API. Opens a new Span, nested under the currently active span (if any, as tracked by an internal ContextVar), and yields it for the duration of the with block.

Parameters

NameTypeRequiredDefaultNotes
namestryes—Human-readable span label.
typestrno"generic"A SpanType value or any custom string.
inputAnynoNoneSet once at span creation; you can also call span.set_input(...) later.
metadataOptional[Dict[str, Any]]noNone (becomes {})Initial metadata dict.
modelOptional[str]noNoneFor LLM spans.
providerOptional[str]noNoneFor LLM spans.

Behavior, step by step (see also the sequence diagram in Architecture):

  1. If self.enabled is False: yield a bare, untracked Span built from the given arguments and return immediately afterward — nothing else in this list happens. This is the "no-op when disabled" path.
  2. Otherwise, read the current span from the ContextVar (may be None).
  3. Build a new Span, setting parent_id to the parent's id if there is a parent.
  4. If there is a parent, overwrite the new span's trace_id with the parent's trace_id (so the whole nested tree shares one trace_id).
  5. Push the new span onto the ContextVar (saving a reset token).
  6. yield the new span to the caller's with block.
  7. On normal completion: call new_span.finish(). On an exception inside the block: call new_span.finish(error=exc), then re-raise — the exception is never suppressed.
  8. In a finally: reset the ContextVar to the parent (or None), and enqueue the finished span for background export.

Raises

span() itself does not raise anything specific to its own logic; any exception raised inside the with block is caught, used to mark the span as errored, and then re-raised unchanged — callers should expect exactly the same exceptions as the code they wrapped.

Thread / async safety: Safe across threads and asyncio tasks for concurrent, independent spans — nesting is tracked via a contextvars.ContextVar, which provides per-task/per-thread isolation. Mutating one specific Span instance concurrently from multiple threads is not guarded against (see Span → Thread safety).

Side effects: enqueues the finished span onto an internal queue, which starts a daemon worker thread the first time it's needed (_ensure_worker).

Example

Python · 5 lines
from zerda.observability import get_tracer

with get_tracer().span("LLM", type="llm", model="gpt-4o") as span:
    answer = call_llm(prompt)
    span.set_output(answer)

record_external_span

Python · 16 lines
def record_external_span(
    self,
    name: str,
    span_type: str,
    start_time: float,
    end_time: Optional[float],
    input: Any = None,
    output: Any = None,
    error: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
    trace_id: Optional[str] = None,
    parent_id: Optional[str] = None,
    span_id: Optional[str] = None,
    model: Optional[str] = None,
    provider: Optional[str] = None,
) -> None

Ingests a span that was already fully executed by another subsystem (the source docstring names, as an example, "the chain framework's own ExecutionTracer") instead of wrapping code with span(). Lets that subsystem's own tracing bridge into this module's exporters without duplicating timing/lifecycle logic.

Parameters

NameTypeRequiredNotes
namestryesSpan label.
span_typestryesNote the parameter name is span_type here, unlike span()'s type.
start_timefloatyesEpoch seconds.
end_timeOptional[float]yes (positional, but accepts None)If None, duration is left None too.
input, outputAnynoUnlike span(), output can be set directly here since the work is already done.
errorOptional[str]noAlready-formatted error string — note this is a str here, not an exception object as with Span.mark_error.
metadataOptional[Dict[str, Any]]noDefaults to {}.
trace_idOptional[str]noIf omitted, a new random one is generated (via the same default_factory Span.trace_id uses) — it is not taken from the current ContextVar, so an external span is not automatically nested under whatever span is "current" in this tracer.
parent_id, span_idOptional[str]noSet directly if provided.
model, providerOptional[str]noFor LLM spans.

Behavior: If self.enabled is False, this is a complete no-op (returns immediately, nothing is constructed or enqueued) — unlike span(), which still yields a usable no-op Span object, this method gives the caller nothing back at all. If enabled, builds a Span directly from the given fields (bypassing the finish() computation — duration is computed manually as end_time - start_time if end_time is given), then enqueues it exactly like span() does.

Returns: None — there is no span object handed back to the caller in either branch.

current_span

Python
def current_span(self) -> Optional[Span]

Returns whatever Span is currently set on the internal ContextVar in the calling thread/task context, or None if there is no open span.

flush

Python
def flush(self, timeout: Optional[float] = None) -> None

Blocks the calling thread until every span currently on the internal queue has been picked up and processed (self._queue.join()).

[!WARNING] timeout is accepted but not enforced The timeout parameter is not passed to queue.Queue.join() (which has no timeout parameter) or used anywhere else in the method body. Calling flush(timeout=5) blocks exactly as long as flush() would — indefinitely, until the queue drains. If a hung exporter is holding the drain thread, flush() will not return within the timeout you passed.

shutdown

Python
def shutdown(self) -> None

Registered with atexit in __init__, so it normally runs automatically on interpreter exit. If the worker thread is alive, pushes a None sentinel onto the queue and joins the thread (up to 5 seconds). Then calls shutdown() on every registered exporter, catching and logging (not re-raising) any exception each one raises.


Module-level: get_tracer

Python
def get_tracer() -> Tracer

Returns the process-wide Tracer singleton, creating it on first call with a double-checked-locking pattern (_tracer_lock) for thread safety. This is the correct way to obtain a Tracer — see Constructor for why calling Tracer() yourself is discouraged.

Module-level: trace

See trace() on the Module-Level Functions page — it's defined in tracing.py but documented alongside the other top-level helpers since it's used the same way (as a public entry point rather than a Tracer method).


Complexity & concurrency summary

OperationTimeNotes
span() enter/exit (disabled)O(1)One Span allocation, nothing else.
span() enter/exit (enabled)O(1) per span, plus O(E) exporter dispatchE = number of registered exporters; dispatch happens on the background thread, not on the caller's critical path.
register_exporter / unregister_exporter / clear_exportersO(N)N = current exporter count (list scan/removal under lock).
flush()Blocks until queue emptiesNo timeout enforcement — see warning above.

Thread safety: Tracer's exporter list is guarded by a threading.Lock; span creation/nesting is guarded by contextvars isolation rather than a lock. Multiprocessing: nothing in the source addresses multiprocessing — the singleton, its queue, and its worker thread are all process-local; spans created in a forked/spawned child process use that child's own fresh Tracer state.


Related: Span · BaseExporter · Module-level functions · Architecture

Related pages