Sampling & Service Metadata
Service metadata
Use configure() to tag trace roots with the service and environment that
produced them. Both values are optional, must be non-empty strings, and are
recorded under metadata.service.
from bir import configure
configure(service_name="rag-api", environment="production")
The same defaults can come from the environment:
export BIR_SERVICE_NAME=rag-api
export BIR_ENVIRONMENT=production
Explicit configuration takes precedence over environment defaults.
Trace source
Use configure(source=...) to tag trace roots with where they originated. The
value is optional, must be a non-empty string, and is recorded under
metadata.source. It is the SDK-side counterpart to the source the Bir
server and dashboard already filter on (the built-in Playground records
"playground"), so SDK traces become filterable by source alongside
product-generated ones.
from bir import configure
configure(source="checkout-api")
The same default can come from the environment:
export BIR_SOURCE=checkout-api
The server matches source by exact, trimmed value, so pick a stable label. An
explicit source in a trace(metadata={"source": ...}) block still wins over
the configured default.
Sampling
Use configure(sample_rate=...) to bound local trace volume. sample_rate is
the probability from 0.0 to 1.0 that a trace is recorded and defaults to
1.0.
from bir import configure
configure(sample_rate=0.1) # record about 10% of traces
Use sample_rules when specific trace roots need a different local rate. Rule
names are exact trace root names: the @observe(name=...) value, the decorated
function name for plain @observe(), or the trace("...") name. A matching
name uses its configured rate; roots with no rule fall back to the global
sample_rate.
from bir import configure
configure(
sample_rate=0.01,
sample_rules={
"checkout": 1.0, # keep every checkout trace
"chatty": 0.0, # drop every chatty trace
},
)
sample_rules is validated at configure() time. Passing sample_rules={}
clears the table; omitting sample_rules leaves the current rules unchanged.
There is no environment variable for per-name rules.
The decision is made once per trace root and inherited by every event under it. A sampled-out trace and all its spans, generations, tool calls, retrievals, and scores write nothing.
Sampling never changes application control flow: a sampled-out function still runs and still raises its own exceptions; only local JSONL writes are skipped.
Set the same default from the environment with:
export BIR_SAMPLE_RATE=0.1
Disabling tracing
sample_rate=0.0 drops every trace, but the clearer way to turn recording off
entirely is the master switch enabled. It is an explicit, intent-revealing
kill switch for a feature flag, an incident toggle, or a test:
from bir import configure
configure(enabled=False) # record nothing
configure(enabled=True) # restore full recording
While disabled, every primitive — @observe, trace, span, generation,
tool_call, retrieval, and score — still runs your code and still raises on
error, but nothing is written, making Bir a true no-op without touching any call
site. The switch is enforced through the same path as sampling, so a trace
already in flight when recording is disabled stops writing immediately, and
re-enabling records traces started afterward. get_current_trace_id() and
get_current_span_id() still return the live ids inside a trace while disabled,
so log correlation keeps working even though nothing is persisted.
Set the same default from the environment with the inverse BIR_DISABLED
variable (a truthy value disables recording):
export BIR_DISABLED=1
An explicit configure(enabled=...) always wins over BIR_DISABLED.
Trace file rotation
The local trace file grows without bound by default. Opt in to size-based rotation:
from bir import configure
configure(max_bytes=5_000_000, backup_count=3)
The active file rotates before a write would exceed max_bytes:
traces.jsonl becomes traces.jsonl.1, the previous .1 becomes .2, and so
on. The default backup_count is 3; backup_count=0 discards the active file
when it fills instead of retaining backups.
Rotation happens on whole-line boundaries, so an event is never split across
files. A single line larger than max_bytes is written whole. Rotation uses the
same lock as writes and adds no dependencies.
Reads use only the active file by default. To reconstruct write order across backups:
from bir import load_events, load_traces
events = load_events(include_rotated=True)
traces = load_traces(include_rotated=True)
A trace can cross a rotation boundary, so it may appear incomplete if only part of the rotated set remains.