Skip to content
RelPrim docsOpenTelemetry

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.