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
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
@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
@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
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
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
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
unregister_exporter
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
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
@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
Behavior, step by step (see also the sequence diagram in Architecture):
- If
self.enabledisFalse: yield a bare, untrackedSpanbuilt from the given arguments and return immediately afterward — nothing else in this list happens. This is the "no-op when disabled" path. - Otherwise, read the current span from the
ContextVar(may beNone). - Build a new
Span, settingparent_idto the parent'sidif there is a parent. - If there is a parent, overwrite the new span's
trace_idwith the parent'strace_id(so the whole nested tree shares onetrace_id). - Push the new span onto the
ContextVar(saving a reset token). yieldthe new span to the caller'swithblock.- On normal completion: call
new_span.finish(). On an exception inside the block: callnew_span.finish(error=exc), then re-raise — the exception is never suppressed. - In a
finally: reset theContextVarto the parent (orNone), 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
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
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
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
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
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]
timeoutis accepted but not enforced Thetimeoutparameter is not passed toqueue.Queue.join()(which has no timeout parameter) or used anywhere else in the method body. Callingflush(timeout=5)blocks exactly as long asflush()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
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
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
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