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.