Quickstart
On this page
This page walks through the three ways you'll actually use this module, in the order you're likely to reach for them.
1. Turn on a backend, change nothing else
If your chains / RAG pipelines / routers already call
Tracer.span() or
Tracer.record_external_span()
internally (that's the intended integration point for the rest of Fennec
Community), then tracing is entirely opt-in from the outside: register an
exporter and everything starts flowing.
from zerda.observability import enable_langsmith
enable_langsmith(api_key="ls__...", project_name="Customer Support")
from zerda.observability import enable_console
enable_console(verbose=True) # prints input/output/metadata too
from zerda.observability import enable_json
enable_json(path="traces.jsonl") # append-only JSON Lines file
Each enable_* call registers an exporter with the global
Tracer singleton, which also turns tracing on
(see Tracer.register_exporter()).
You can register more than one — a span is delivered to every registered
exporter.
2. Wrap your own code in a span
Use Tracer.span() as a context manager anywhere you
want a traced unit of work — a tool call, a retrieval step, a manual LLM
call:
from zerda.observability import get_tracer
with get_tracer().span("MyTool", type="tool") as span:
span.set_input(query)
result = do_work(query)
span.set_output(result)
If an exception is raised inside the with block, the span is
automatically marked as failed (span.error, span.stack_trace are
populated) and the exception still propagates — tracing never
swallows your errors. See Tracer.span() for the
full behavior.
3. Trace a whole function with a decorator
from zerda.observability import trace
@trace("MyTool", type="tool")
def run_tool(query: str) -> str:
return do_work(query)
This is sugar over step 2: it opens a span named "MyTool" around the
call, sets the span's output to the function's return value, and returns
that value unchanged. See trace().
Multiple backends at once
from zerda.observability import get_tracer, ConsoleExporter, JSONExporter
get_tracer().register_exporter(ConsoleExporter())
get_tracer().register_exporter(JSONExporter(path="traces.jsonl"))
Every span is now printed to stdout and appended to traces.jsonl.
Turning tracing off again
from zerda.observability import disable_tracing, is_tracing_enabled
disable_tracing()
is_tracing_enabled() # False
disable_tracing() keeps your registered exporters in place — a later
get_tracer().enable() resumes exporting to them without re-registering
anything. See Tracer.disable().
What "no exporter registered" costs you
Until an exporter is registered, get_tracer().span(...) returns a
Span that is never tracked, nested, or exported — cheap enough to leave
sprinkled through hot paths unconditionally. See
Architecture → No-op when disabled
for exactly what that path does and doesn't do.
Next: Architecture for how the pieces fit together, or jump straight to the API Reference.