OpenTelemetry
Trace your decisions as OpenTelemetry spans so they sit on the same timeline as every other service in your stack.
otel For: platform & observability
Spans and decision records are complementary
These two layers answer different questions, and you usually want both.
| Layer | Answers | Lives where |
|---|---|---|
| OTel span (timeline) | When did it run, how long, in what order, alongside which other services | Your tracing backend |
| Briefcase AI decision record (governance context) | Why this output — inputs, outputs, confidence, timing, full reproducible context | Your exporter or store |
The span is a lightweight timeline marker; the decision record is the deep governance context. They come from two different lines of your code: @capture writes the record, and a span you open writes the timeline.
flowchart LR
A[classify_ticket] -->|"@capture"| D[Decision record: governance context]
A -->|"start_as_current_span"| C[OTel span: timeline]
C --> E[Tracing backend]
D --> F[Exporter / store]
Install
pip install briefcase-ai[otel]Without OTel vs with OTel
Decisions are still captured and exported — you can inspect them through an exporter. But there is no span on your distributed trace, so the decision is invisible to your tracing UI and you cannot see where it sat relative to upstream and downstream services.
from briefcase import capture
@capture(decision_type="ticket_triage")def classify_ticket(text): # call your model here return "account_access"
classify_ticket("reset my password")Open a span around the decision and it becomes part of the trace, correlated with the rest of your services. Spans flow through whatever OTel TracerProvider your application has configured.
from briefcase import capturefrom briefcase.otel import get_tracer
tracer = get_tracer("briefcase")
@capture(decision_type="ticket_triage")def classify_ticket(text): with tracer.start_as_current_span("classify_ticket") as span: span.set_attribute("briefcase.decision_type", "ticket_triage") category = "account_access" # call your model here span.set_attribute("briefcase.outcome", category) return category
classify_ticket("reset my password")How it works
-
Get a tracer —
get_tracer("briefcase")returns a standard OpenTelemetry tracer. -
Open a span around the decision and attach attributes that describe it.
-
Correlate — propagate trace context to downstream services so spans join one trace.
-
Inspect in your tracing backend, then match the decision span to the full decision record an exporter shipped.
Get a tracer
get_tracer() returns a standard OpenTelemetry tracer. Use it to open spans around the work you want to trace.
from briefcase.otel import get_tracer
tracer = get_tracer("briefcase")
with tracer.start_as_current_span("classify_ticket") as span: span.set_attribute("briefcase.decision_type", "ticket_triage") category = "account_access" span.set_attribute("briefcase.outcome", category)get_tracer(name="briefcase") is the only public symbol in briefcase.otel.
Semantic conventions
Briefcase AI ships span-attribute conventions under briefcase.semantic_conventions. Each submodule defines the attribute keys for one subsystem, so the attributes you emit are consistent across services instead of ad-hoc strings.
Two submodules you will reach for most on the triage path:
from briefcase.semantic_conventions import workflow, rag
# Tag the retrieval span on the triage path with RAG attribute keyswith tracer.start_as_current_span("retrieve-ticket-history") as span: # use rag.* keys for the retrieval step, workflow.* keys for the workflow it runs in ...The full set of submodules:
briefcase.semantic_conventions.lakefsbriefcase.semantic_conventions.workflowbriefcase.semantic_conventions.ragbriefcase.semantic_conventions.external_databriefcase.semantic_conventions.coworkbriefcase.semantic_conventions.agent_statebriefcase.semantic_conventions.bitemporalbriefcase.semantic_conventions.routing_policybriefcase.semantic_conventions.validation
Import the module for the subsystem you are instrumenting and use its attribute keys when setting span attributes. The workflow keys line up with the multi-agent correlation surface.
Ship decisions to an external observability sink
The span describes the work; the decision record carries the captured inputs, outputs, and timing. To forward those records into an external observability sink you already operate — a log aggregator, a message queue, an analytics pipeline — subclass BaseExporter and implement its three async methods.
from typing import Anyfrom briefcase import setup, capturefrom briefcase.exporters import BaseExporter
class SinkExporter(BaseExporter): async def export(self, decision: Any) -> bool: # ship the decision record to your external observability sink here # e.g. post to a collector, enqueue, or forward to a log pipeline return True
async def flush(self) -> None: pass
async def close(self) -> None: pass
setup(exporter=SinkExporter())
@capture(decision_type="ticket_triage")def classify_ticket(text): # call your model here — span streams to your tracer, record streams to the sink return "account_access"
classify_ticket("reset my password")For the stock exporters (ConsoleExporter, JSONLFileExporter, MemoryExporter) and the one-line briefcase.observe() setup, see Exporters.
Limits
@capture itself does not emit a span. Open spans explicitly for arbitrary
functions. For supported AI frameworks, Framework Auto-Instrumentation
installs their native handler or trace hook and emits decision records without
decorating each call.
Nothing links a span to its decision record. The record’s decision_id is
generated inside the decorator and is not visible to the function body, so there is
no field the two share. Correlate with an id you control: pass a request id into the
function so it lands in inputs, and set the same value as a span attribute.
get_tracer is a thin passthrough. It returns whatever
opentelemetry.trace.get_tracer returns, which is a no-op tracer until your
application configures a TracerProvider. Spans then go wherever that provider
sends them; Briefcase AI configures no exporter of its own.
Semantic conventions are constants, not behavior. Each submodule of
briefcase.semantic_conventions is a set of uppercase key names such as
workflow.WORKFLOW_AGENT_CHAIN and rag.RAG_DOCUMENT_VERSION. Importing one sets
no attributes; you pass the keys to span.set_attribute yourself. The one surface
that emits them for you is
workflow correlation, which writes workflow.* keys onto
its own root span.
API reference
briefcase.otel has the full surface, which is one function.
Key symbols
briefcase.otel.get_tracer(name="briefcase")— return an OpenTelemetry tracer.briefcase.exporters.BaseExporter— base class for custom exporters; implementexport,flush,close.briefcase.semantic_conventions.*— attribute-key modules for each subsystem.
Best practices
-
Sample high-volume paths — use sampling so a busy triage queue does not overwhelm your backend.
-
Set resource attributes — identify the service and environment so spans are easy to filter.
-
Use the semantic-convention keys — consistent attribute names make spans queryable across services.
-
Pair spans with an exporter — the span gives you the timeline, the exported record gives you the governance context.
Where this fits
OpenTelemetry is part of operating a governed system in production: the timeline view that sits next to your cost and event signals.