FAQ & Troubleshooting
On this page
Why don't I see any spans?
Check, in order:
- Is an exporter registered?
Tracer.enabledisFalseuntil at least one exporter is registered — callis_tracing_enabled()to check. Registering via anyenable_*()helper orget_tracer().register_exporter(...)also turns tracing on. - Did you call
disable_tracing()and forget to re-enable? Exporters stay registered across adisable()/enable()cycle — callget_tracer().enable()(not re-registration) to resume. - Did the process exit before spans were flushed? Export happens on
a background thread; call
get_tracer().flush()before your process exits if you need a guarantee that every queued span has been handed to your exporters.Tracer.shutdown()(registered withatexit) normally does this automatically, but abrupt process termination (e.g.os._exit(), a killed container) can skipatexithandlers entirely. - Is the exporter itself failing silently? Exporter exceptions are
caught and logged via Python's
loggingmodule (logger.exception(...)inTracer._drain_loop), not raised into your code — check your logs at thezerda.observability.tracinglogger for"Exporter %s failed to export span %s".
enable_langsmith() raises ImportError
The langsmith package isn't installed. It's an optional dependency —
install it with pip install langsmith. See
Installation → Optional dependency: LangSmith.
My spans aren't nested the way I expected
Nesting (parent_id) is tracked automatically only for spans opened via
Tracer.span(), using a contextvars.ContextVar —
so a child with get_tracer().span(...) block only becomes a child of
the currently open span, which depends on lexical/call nesting and
async task boundaries. Two common surprises:
- Spans reported via
record_external_span()are never automatically nested under whatever is "current" — that method doesn't read theContextVarat all. Passparent_idandtrace_idexplicitly if you want a specific relationship. - Constructing a bare
Span(...)yourself always gets a fresh randomtrace_idandparent_id=None— grouping only happens throughTracer.span()'s logic of copying the parent'strace_idonto the child.
Does flush(timeout=...) actually time out?
No — per the source, Tracer.flush() calls self._queue.join(), and the
timeout parameter is not forwarded anywhere (queue.Queue.join() takes
no timeout argument). Passing a timeout value currently has no effect;
flush() blocks until the queue is empty regardless. See the warning on
Tracer.flush.
Is this OpenTelemetry-compatible out of the box?
No. Span here is this module's own, independent shape — there's no
built-in OTel exporter in the provided source. You can write one by
subclassing BaseExporter; see the
Cookbook for a sketch of what
that integration point looks like.
Is it safe to leave tracer.span(...) calls in code paths that never register an exporter?
Yes — that's the explicit design intent stated in the source docstring:
until an exporter is registered, span() returns a Span that's never
tracked or exported, and constructing it is the only cost. See
Architecture → No-op when disabled.
Does this work with asyncio and threads?
Nesting/grouping (parent_id / trace_id) uses a
contextvars.ContextVar, which is per-thread and per-asyncio.Task by
design — concurrent traces in different tasks/threads don't cross-link
with each other. See Architecture → Context propagation.
The source does not address multiprocessing; a forked or spawned child
process gets its own independent Tracer singleton and queue.
Can I use two separate Tracer instances in the same process?
Nothing prevents constructing Tracer() directly, but doing so gives you
an isolated exporter list and queue, disconnected from whatever the rest
of the framework calls get_tracer() to reach. Unless you have a
specific reason to isolate tracing (e.g. a test harness), use
get_tracer() everywhere so all spans share one set of exporters. See
Tracer → Constructor.
If this page didn't answer your question, the API Reference documents every method's exact behavior directly from the source, and Architecture explains how the pieces interact.