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 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. Both flow out through Briefcase, so a span you find in your tracing UI can be matched to a decision record you can replay and verify.
flowchart LR
A[classify_ticket] --> B[Briefcase]
B --> C[OTel span: timeline]
B --> D[Decision record: governance context]
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 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.
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.