Skip to Content

How do I attach an end user to a trace with the OpenTelemetry Python SDK?

Rius reads the user.id span attribute to tie a request to an end user, so you attach a user by setting user.id on the spans you create with the OpenTelemetry Python SDK. A plain OpenTelemetry Python setup does not set it for you, so you add one line per span. Spans that carry the attribute show up under that user in Rius, with their traces, failures, cost and time to first token.

How it works

  • Rius reads user.id from each span, not from the resource. Set it on every span in scope, the root span included, because the platform reads it per span.
  • If user.id is missing, Rius also reads enduser.id, then enduser.pseudo.id. A span with none of the three counts as anonymous.
  • The OpenTelemetry Python setup that sends traces to Rius configures the endpoint, the key and service.name. It sets no user, so every span stays anonymous until you add the attribute.
  • Use an opaque id, such as your own database key, rather than an email address.
  • session.id works the same way and groups the traces of one conversation or agent run. With no session.id, each trace is its own session.
  • The Rius SDK for Python has a user scope, rius.user, that stamps the attribute for you. The steps below are for when you use the OpenTelemetry SDK on its own.

Set it up

  1. Register a tracer provider that exports to Rius. Install opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http, set the OTEL_EXPORTER_OTLP_* variables, and run this. The variables only configure the exporter, so the provider code is what sends spans. The full variable list is on the Vanilla OpenTelemetry SDKs page.
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. Set user.id on each span you start, starting with the root span of the run. Pass the same id to the child spans, so the attribute is on every span of the trace. Replace user_id and the span names with your own.
from opentelemetry import trace tracer = trace.get_tracer("my-agent") def handle(query: str, user_id: str) -> str: with tracer.start_as_current_span("handle") as root: root.set_attribute("user.id", user_id) with tracer.start_as_current_span("chat gpt-4o") as llm: llm.set_attribute("user.id", user_id) # ... call the model and set the model and token attributes ... return "done"
  1. Add session.id the same way if you want conversations grouped, using one value per conversation.

  2. Run one request, open Traces in the console, and open the trace. The span’s attributes should list user.id with your value, and the user should appear on the Users page.

If spans arrive but the user is missing, check that the root span carries user.id, then that the child spans do too. The trace list shows each trace under the user of its root span. The span attribute reference lists the user and session keys in full.

What you get

  • Each end user your agents served appears with their traces, failures, cost and time to first token.
  • Cost per user comes from the model and token attributes on the same spans. Rius computes it, so you never send a cost value.
  • Spans without a user id stay visible as anonymous, so adding the attribute later does not break existing traces.
  • Sessions group a conversation’s traces when you set session.id too.

For the wider picture of sending LLM traces from an existing OpenTelemetry setup, see Trace LLM calls with OpenTelemetry.

When something else fits

  • If you would rather not set the attribute on every span yourself, use the Rius SDK for Python and wrap the work in rius.user, which stamps user.id on the spans in scope.
  • If your spans come from OpenInference, its using_user context manager stamps user.id, which Rius reads directly. Laminar’s lmnr.association.properties.user_id is mapped to the user at ingest.

Still stuck? Get help

Last updated on