Skip to content

OpenTelemetry Observer

Import: from selectools.observe import OTelObserver Stability: beta Added in: v0.21.0

OTelObserver maps the 45 selectools observer events to OpenTelemetry spans, following the OpenTelemetry GenAI semantic conventions. Once attached, every agent run, LLM call, and tool execution becomes a span you can ship to Jaeger, Tempo, Honeycomb, Datadog, Grafana, or any other OTLP-capable backend.

otel_quick.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter

from selectools import Agent, AgentConfig, OpenAIProvider, tool
from selectools.observe import OTelObserver

# 1. Configure your OTel SDK once at process start
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))

# 2. Attach the observer
@tool()
def search(query: str) -> str:
    return f"Results for {query}"

agent = Agent(
    tools=[search],
    provider=OpenAIProvider(),
    config=AgentConfig(observers=[OTelObserver()]),
)

result = agent.run("Find articles about Python")
# Spans now flow to your OTel exporter

See Also

  • Langfuse - Alternative observer focused on LLM tracing
  • Trace Store - Persist agent traces to disk or SQLite
  • Audit - JSONL audit logs

Install

pip install "selectools[observe]"

The [observe] extras include opentelemetry-api>=1.20.0. selectools does not ship opentelemetry-sdk or any exporters — bring your own. Common choices:

pip install opentelemetry-sdk opentelemetry-exporter-otlp     # OTLP
pip install opentelemetry-sdk opentelemetry-exporter-jaeger   # Jaeger

This separation lets you reuse whatever exporter the rest of your stack already uses without selectools pinning a transitive dependency.


Span Hierarchy

Each agent run becomes a span tree:

agent.run                              ← root span
├── gen_ai.llm.call                    ← per LLM round-trip
│   └── gen_ai.tool.execution          ← per tool call
├── gen_ai.llm.call
└── ...
Span name Attributes
agent.run gen_ai.system="selectools", gen_ai.usage.total_tokens, gen_ai.usage.cost_usd
gen_ai.llm.call gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens
gen_ai.tool.execution gen_ai.tool.name, gen_ai.tool.duration_ms, gen_ai.tool.success

Constructor

OTelObserver(tracer_name: str = "selectools")
Parameter Description
tracer_name Name passed to trace.get_tracer(). Use this to scope spans by service in multi-app processes.

Async

For agent.arun() / agent.astream() use the async variant:

from selectools.observe.otel import AsyncOTelObserver
agent = Agent(..., config=AgentConfig(observers=[AsyncOTelObserver()]))

API Reference

Symbol Description
OTelObserver(tracer_name) Sync observer for agent.run() / agent.stream()
AsyncOTelObserver(tracer_name) Async observer for agent.arun() / agent.astream()

# Script Description
87 87_otel_observer.py Wire selectools traces into an OTLP exporter