OpenTelemetry
Mapping Interbolt's setup, factory, and decorator patterns onto Phoenix/OpenTelemetry vocabulary, and wiring OTelReporter next to an already-instrumented app.
OpenTelemetry
Already instrumenting your agent with Phoenix or plain OpenTelemetry? This
page covers both: the vocabulary mapping if you are coming from that world,
and the mechanics of wiring OTelReporter into an app that already has
tracing set up.
Coming from Phoenix / OpenTelemetry
Interbolt borrows the OpenTelemetry provider shape: a setup call that returns a provider, a factory method that hands back a per-caller handle, a decorator family, and an extension point for attaching output processors after the fact. If you have already instrumented an agent with Phoenix or plain OpenTelemetry, the setup, factory, and decorator patterns below are the ones you already know. What differs is what the primitives do once you call them: OpenTelemetry observes execution and records what happened; Interbolt gates execution and decides, before the call runs, whether it is allowed to happen at all.
The mapping
| Phoenix / OpenTelemetry | Interbolt |
|---|---|
register(...) returns a tracer provider | configure(...) returns a Runtime, not named register because it configures an enforcement authority with a policy, not an attachment to a collector |
provider.get_tracer(name) | agent(agent_id) or runtime.agent(agent_id), returning a durable per-agent handle bound to an identity rather than a tracer bound to a module name |
provider.add_span_processor(...) | runtime.add_reporter(...) |
trace.get_tracer_provider() | get_runtime() |
@tracer.tool, @tracer.llm decorators | @handle.guard, @handle.track_model_call; @handle.guard runs before the call and can raise, unlike any OTel decorator (see "Where Interbolt intentionally differs" below) |
using_session(...) context manager | agent_context(...) / agent_context_sync(...), which binds an identity that changes enforcement outcomes for the run rather than an attribute attached to spans |
OTEL_* / PHOENIX_* env vars | INTERBOLT_* env vars (INTERBOLT_MODE, INTERBOLT_AUDIT, INTERBOLT_RECURSION_DEPTH) |
| In-memory exporter for tests | InMemoryReporter |
| Spans in your collector | OTelReporter (span events inside your existing traces; interbolt[otel] extra) |
The handle in row two is the closest structural analog to get_tracer, but
it is not the default way to guard a tool. Bare @guard with identity bound
at the call site by agent_context is; see
Identity.
Side by side
A tool instrumented with Phoenix, the way you would already write it:
from phoenix.otel import register
tracer_provider = register(project_name="support-agent")
tracer = tracer_provider.get_tracer(__name__)
@tracer.tool
def send_email(to: str, body: str) -> None:
...
send_email(to="user@example.com", body=summary)The same tool, with Interbolt's gate added on top of the existing Phoenix instrumentation:
from interbolt import OTelReporter, Policy, configure
runtime = configure(policy=Policy.from_file("policy.yaml"))
runtime.add_reporter(OTelReporter())
support = runtime.agent("support-agent")
@tracer.tool
@support.guard
def send_email(to: str, body: str) -> None:
...
send_email(to="user@example.com", body=summary)@tracer.tool stays outermost, so its span is already open and recording
by the time @support.guard calls check(). OTelReporter then attaches
the resulting decision as an event on that same span, so the Phoenix trace
and the Interbolt decision live in one place with no separate exporter to
configure. See What appears in the trace
for the exact event shape.
Where Interbolt intentionally differs
@guardis a gate rather than an observer. It evaluates policy before the wrapped call executes and raisesPolicyViolationon block. No OTel decorator alters control flow; this one exists to enforce it.- No
uninstrument(), no reporter removal, no mutable policy or mode on a live runtime. Detaching an observability instrument is harmless. Silently detaching a security gate creates a way to bypass enforcement. Enforcement inputs change only through a newconfigure()call. - Fail-closed by default. Under
Mode.ENFORCE, an evaluation error blocks rather than proceeds. Observability tooling instead fails open; an enforcement layer must not.
Wiring OTelReporter
If you are not coming from Phoenix or OTel, or just want the reporter wiring
on its own: add OTelReporter next to your existing instrumentation and
Interbolt's decisions show up as span events inside the traces you already
have.
from interbolt import OTelReporter, Policy, configure
runtime = configure(policy=Policy.from_file("policy.yaml"))
runtime.add_reporter(OTelReporter())
# elsewhere, inside a span your own instrumentation already opened:
with tracer.start_as_current_span("handle_request"):
send_email(to=..., body=...) # a guarded call inside the spanWhat appears in the trace
When a guarded call happens inside a recording span, which is the common
case since your framework or app already wraps the request, turn, or tool
dispatch, the decision, finding, or endorsement is added to that span as an
event named interbolt.decision, interbolt.finding, or
interbolt.endorsement, carrying interbolt.*-namespaced attributes (tool,
action, matched rule, mode, sources; see the
full mapping table).
No new span is created; the decision rides inside the span your own code
already started.
Two fields are deliberately never exported: contributing_labels, which is
unbounded, and matched_condition, whose CEL text may embed a literal you
did not intend to ship to a third-party backend. Both remain in the native
records (see Events).
No span active, or no provider configured
With no recording span active, OTelReporter opens and immediately closes a
small span of its own, named the same way, so the decision is still exported
rather than silently dropped. If no TracerProvider is configured at all,
this is a no-op by OpenTelemetry's own design, exporting nothing and raising
nothing. Either way, OTelReporter never blocks or delays a decision, since
attaching a span event is an in-memory operation rather than I/O.
interbolt[otel] depends only on opentelemetry-api. Wiring an actual
exporter (OTLP, console, or otherwise) is your own OpenTelemetry SDK setup,
unrelated to Interbolt.
Note that trace_id and span_id are also recorded on every native record
(Event, Finding, Endorsement) when a span is active, so a
JsonlReporter log can be correlated back to your traces even without
OTelReporter attached.
Next
See Events for the versioned record schema and the complete OpenTelemetry attribute mapping.