Skip to Content

How do I record tool calls with the OpenTelemetry Python SDK?

Rius records a tool call from the OpenTelemetry Python SDK when the span carries gen_ai.operation.name set to execute_tool and a gen_ai.tool.name. The SDK and exporter do not set these for you, so you add them on each tool span. Rius then shows the span as a tool call in trace views and kind filters.

How it works

A tracer provider and an OTLP exporter only ship spans. They do not mark any span as a tool call. Rius classifies a span from one of two attributes, and you can set either:

Set one and Rius derives the other. Setting both, as the Rius SDKs do, is the clearest choice.

The attributes a tool span carries:

  • gen_ai.tool.name: the tool’s own name, never the span name.
  • gen_ai.tool.call.id: the id of the model’s tool call this execution answers.
  • gen_ai.tool.type: for example function, extension or datastore.
  • gen_ai.tool.call.arguments and gen_ai.tool.call.result: the input and output. Rius moves them to input.value and output.value, so search and filter on those keys.
  • error.type: a low-cardinality error class, such as tool_error, on a span that ended in an error.

Name the span the way the GenAI conventions ask, the operation followed by the tool: execute_tool get_weather. A tool called over MCP is an execute_tool span too. Add mcp.method.name with the value tools/call so Rius can tell MCP tools from local ones.

Tool spans carry no tokens, so they have no cost. Rius computes cost from the model name and token counts on the spans for model calls.

Set it up

  1. Install opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http, then register a tracer provider with the OTLP exporter. Without a registered provider, spans go nowhere. Point the exporter at Rius with OTEL_EXPORTER_OTLP_ENDPOINT set to https://ingest.eu.console.rius-glassflow.com, OTEL_EXPORTER_OTLP_PROTOCOL set to http/protobuf, and OTEL_EXPORTER_OTLP_HEADERS set to authorization=Bearer ri_.... Set OTEL_SERVICE_NAME to your agent’s name, because Rius groups traces by service name. The Vanilla OpenTelemetry SDKs page covers this setup in full.
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. Wrap each tool function in a span that sets the tool attributes. Start it inside a root span for the agent run, and the tool call nests under that run in one trace. The names get_weather, tool_call_id and run_agent below are your own.
import json from opentelemetry import trace tracer = trace.get_tracer("my-agent") def get_weather(city: str, tool_call_id: str) -> str: with tracer.start_as_current_span("execute_tool get_weather") as span: span.set_attributes({ "openinference.span.kind": "TOOL", "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": "get_weather", "gen_ai.tool.type": "function", "gen_ai.tool.call.id": tool_call_id, "gen_ai.tool.call.arguments": json.dumps({"city": city}), }) try: result = "sunny, 21 C" # call your real tool here except Exception: span.set_attribute("error.type", "tool_error") raise span.set_attribute("gen_ai.tool.call.result", result) return result def run_agent(query: str) -> str: with tracer.start_as_current_span("invoke_agent my-agent"): return get_weather("Berlin", "call_1")
  1. Run the agent once, open Traces in the console, and open the trace. The tool span appears under the run with its tool name, input and output. The span attribute reference lists every attribute Rius reads, including the message shape for model calls.

What you get

  • Each tool call shows as its own span, with the tool name, input, output and timing, nested under the agent run.
  • The tool span has the Tool kind, so the All kinds menu in the Spans tab finds it. The Tool Loop alert counts only spans of kind TOOL, so only tool spans marked this way can trigger it.
  • A failed tool call carries error.type, so it shows in error analysis.
  • An MCP tool call carries mcp.method.name, so context attribution can report the MCP share of a trace.

The Trace LLM calls with OpenTelemetry guide shows how to send the model calls that request these tools from the same setup.

When something else fits

  • If your tools run through a library that already emits tool spans, such as the OpenInference or OpenLLMetry instrumentors, check what it sets before adding spans by hand. Rius maps their conventions onto the same attributes at ingest.
  • If you use the Rius SDK for Python, observe with the TOOL kind creates the execute_tool span for you, so you do not set the attributes yourself.

Still stuck? Get help

Last updated on