Skip to Content

How do I group spans into sessions with the OpenTelemetry Python SDK?

Rius groups spans into sessions by the session.id attribute, so with the OpenTelemetry Python SDK you set session.id on every span of a conversation or agent run. The Python SDK does not set it for you, so without it each trace is its own session. Rius also reads gen_ai.conversation.id when session.id is absent.

How it works

Rius reads the session from each span, not from the trace or the resource. The lookup runs in this order:

  • session.id on the span.
  • gen_ai.conversation.id on the span, the session key of the OpenTelemetry GenAI conventions.
  • The trace id, so a span with neither key makes its trace a session of its own.

A standard OpenTelemetry Python setup sets neither key. It configures the exporter, the service name and the tracer provider, and none of those carry a session. Spans from a vanilla setup therefore arrive correctly but each trace appears alone until you add the attribute.

Set the attribute on every span in the trace, the root included. A span that misses it falls back to gen_ai.conversation.id and then to its trace id, so a session set only on the root can be split. The value is a string you choose, such as a conversation id from your own app. Reuse the same value for every trace in the same session.

Set it up

  1. Register a tracer provider with the OTLP exporter, if you have not already. This is the setup from Vanilla OpenTelemetry SDKs, which also lists the endpoint, protocol and header variables the exporter reads. The Trace LLM calls with OpenTelemetry guide compares the other ways to send traces.
from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # With no arguments, the exporter reads the OTEL_EXPORTER_OTLP_* variables provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider)
  1. Pick the session id for the conversation or run. Use the same string for every trace that belongs to it.

  2. Set session.id on each span you create, the root and every child. The span names here follow the GenAI naming, the operation followed by what it acted on. Replace the id and names with your own.

from opentelemetry import trace tracer = trace.get_tracer("my-agent") session_id = "conversation-123" with tracer.start_as_current_span("invoke_agent planner") as root: root.set_attribute("session.id", session_id) with tracer.start_as_current_span("chat gpt-4o") as llm: llm.set_attribute("session.id", session_id) # ... call the model, then set model and token attributes
  1. If your instrumentation already follows the GenAI conventions and sets gen_ai.conversation.id, you can skip step 3. Rius reads it as the session when session.id is absent.

  2. Run two traces with the same id, open the trace list in the console, and check that they show as one session. For the full attribute contract, see the span attribute reference.

What you get

  • Traces that share a session.id group into one session, so you can follow a whole conversation or agent run instead of isolated traces.
  • A predictable fallback. A span with no session key is grouped by its trace id, so nothing is dropped.
  • Spans that already carry gen_ai.conversation.id group into sessions with no code change.
  • The same session grouping for spans from other senders, because Rius reads session.id from OpenInference and the other supported conventions as well.

If you also want to see which end user a session served, set user.id on the same spans. A span with no user id counts as anonymous.

When something else fits

If you use the Rius SDK for Python, rius.session stamps session.id on the spans in its scope for you, and RIUS_SESSION_ID sets one session for a whole process. Use the manual attribute when you send spans from a vanilla OpenTelemetry setup, or when your existing instrumentation already sets gen_ai.conversation.id.

Still stuck? Get help

Last updated on