Observability
OpenTelemetry
Export RelPrim's structured events to the active OpenTelemetry span with OpenTelemetryEventSink, without RelPrim configuring any tracing infrastructure.
OpenTelemetryEventSink adds every RelPrim structured event
(attempts, retries, validation outcomes, fallbacks, circuit-breaker rejections,
rate-limit decisions) to the span that is active when the operation runs. Your
existing traces then show what happened inside each call.
Install
pip install "relprim[otel]"
The extra installs opentelemetry-api (>=1.24.0,<2.0.0) only. Your application
provides the SDK and exporter, for example:
pip install opentelemetry-sdk
Usage
from opentelemetry import trace
from relprim import EventEmitter, resilient
from relprim.opentelemetry import OpenTelemetryEventSink
tracer = trace.get_tracer("my.application")
otel_events = EventEmitter(sinks=(OpenTelemetryEventSink(),))
@resilient(retries=3, timeout=10, events=otel_events)
async def call_provider(prompt: str) -> str:
return await provider.generate(prompt)
with tracer.start_as_current_span("handle_request"):
result = await call_provider("Summarize this ticket")
The events land on the handle_request span. The same emitter works with the
builder API through .with_events(otel_events).
For a quick local check, a console exporter is enough:
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
tracer = tracer_provider.get_tracer("my.application")
Without an active span
When no recording span is active, emit() does nothing and the operation
continues normally. That covers tracing being disabled, the span not being
sampled, and no SDK being installed, so the sink is safe to configure
unconditionally.
Event names and attributes
Event names are the EventType values prefixed with relprim., for example
relprim.attempt.failed or relprim.retry.scheduled. Which events appear
depends on the configured policies and the execution path.
Every span event has these attributes:
| Attribute | Value |
|---|---|
relprim.event.type |
The EventType value, such as retry.scheduled. |
relprim.operation.name |
The operation name. |
relprim.payload.<key> |
One attribute per payload key. |
relprim.event.type = "retry.scheduled"
relprim.operation.name = "call_provider"
relprim.payload.attempt_number = 1
relprim.payload.next_attempt_number = 2
relprim.payload.delay_seconds = 0.08
relprim.payload.rate_limited = false
Payload values that are None are omitted, because OpenTelemetry attributes
cannot be None. All other values are strings, booleans, integers or finite
floats, which are valid attribute values as they are. The available keys are
the payload keys documented in Structured events.
Timestamps
Each span event uses the event’s own timestamp, converted to integer nanoseconds without floating-point rounding, not the time at which an exporter processes it.
Several sinks
The OpenTelemetry sink can be combined with other sinks. They are awaited in the order given:
from relprim import EventEmitter, SQLiteEventStore
from relprim.opentelemetry import OpenTelemetryEventSink
events = EventEmitter(sinks=(SQLiteEventStore("relprim-events.db"), OpenTelemetryEventSink()))
Concurrency and cancellation
Concurrent operations in different tasks or spans add events only to their own
current span. emit() does not await anything, so cancellation cannot interrupt
it halfway; only the events emitted before a cancellation appear on the span.
Scope
The sink writes events to an existing span. It does not create spans for
operations or attempts, record exception objects, export metrics, use the logs
signal, or ship an exporter. The core package never imports OpenTelemetry;
only relprim.opentelemetry does, and importing it without the extra raises
ModuleNotFoundError with the matching pip install command.
