Skip to content
Docs for briefcase-ai v3.3.0see what’s new.

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.

LayerAnswersLives where
OTel span (timeline)When did it run, how long, in what order, alongside which other servicesYour tracing backend
Briefcase decision record (governance context)Why this output — inputs, outputs, confidence, timing, full reproducible contextYour 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

Terminal window
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")

How it works

  1. Get a tracerget_tracer("briefcase") returns a standard OpenTelemetry tracer.

  2. Open a span around the decision and attach attributes that describe it.

  3. Correlate — propagate trace context to downstream services so spans join one trace.

  4. 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 keys
with 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.lakefs
  • briefcase.semantic_conventions.workflow
  • briefcase.semantic_conventions.rag
  • briefcase.semantic_conventions.external_data
  • briefcase.semantic_conventions.cowork
  • briefcase.semantic_conventions.agent_state
  • briefcase.semantic_conventions.bitemporal
  • briefcase.semantic_conventions.routing_policy
  • briefcase.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 Any
from briefcase import setup, capture
from 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; implement export, flush, close.
  • briefcase.semantic_conventions.* — attribute-key modules for each subsystem.

Best practices

  1. Sample high-volume paths — use sampling so a busy triage queue does not overwhelm your backend.

  2. Set resource attributes — identify the service and environment so spans are easy to filter.

  3. Use the semantic-convention keys — consistent attribute names make spans queryable across services.

  4. 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.