How do I set the tool call attribute on an OpenTelemetry span in Python?
Rius reads a tool call from a span that carries gen_ai.operation.name set to execute_tool, with the tool’s name in gen_ai.tool.name and the model’s call id in gen_ai.tool.call.id. In Python you set these with span.set_attributes(...) from the standard OpenTelemetry API. Rius then shows the span as a tool step in your trace, with its input and output rendered like any other span.
How it works
Rius classifies spans by two attributes, openinference.span.kind and gen_ai.operation.name. Set either one and Rius derives the other, so execute_tool becomes TOOL. Setting both keeps the span unambiguous.
The keys that describe a tool call all go on the same span:
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 that this execution answers.gen_ai.tool.type:function,extension,datastoreand so on.gen_ai.agent.name: the agent executing the tool.gen_ai.tool.call.argumentsandgen_ai.tool.call.result: what the tool received and returned.error.type: a low-cardinality class such astool_error, if the tool failed.
Rius moves the arguments and result onto input.value and output.value. A native input.value or output.value wins, and the two source keys are removed either way, so search on input.value and output.value.
Name the span the way the GenAI conventions ask, the operation followed by what it acted on, for example execute_tool get_weather. Rius does not require it, but it keeps traces from mixed senders readable.
The model’s request for a tool lives on the LLM span, not the tool span. There it appears as a tool_call part inside gen_ai.output.messages, and the tool’s answer is sent back as a tool_call_response part in the next model call’s gen_ai.input.messages. The id in those parts is the one you reuse in gen_ai.tool.call.id. For the full message shape, see the span attribute reference.
Set it up
- Register a tracer provider that exports to Rius. This is the standard Python setup from Vanilla OpenTelemetry SDKs: install
opentelemetry-sdkandopentelemetry-exporter-otlp-proto-http, set theOTEL_EXPORTER_OTLP_*variables described there, then register the provider. The variables take effect only through this provider.
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)- Wrap each tool execution in its own span and set the tool call attributes.
get_weatherstands for your own function, and the values are examples.
import json
from opentelemetry import trace
tracer = trace.get_tracer("my-agent")
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.call.id": "call_1",
"gen_ai.tool.type": "function",
"gen_ai.tool.call.arguments": '{"city": "Paris"}',
})
result = get_weather("Paris")
# Attribute values must be strings, numbers, booleans or lists of them
span.set_attribute("gen_ai.tool.call.result", json.dumps(result))-
Open the tool span while the agent run’s span is current, so it nests under it and arrives as one trace. Wrap each agent run in a single root span, because the root’s name, duration and status are what run-level views show.
-
If the tool is called over the Model Context Protocol (MCP), also set
mcp.method.nametotools/call. Rius uses it to tell MCP tools from local ones when it reports how many context tokens MCP tools took. -
If the tool fails, call
span.record_exception(...)and seterror.type. The standardexceptionevent surfaces in error analysis.
For the wider walkthrough of sending any trace, see Trace LLM calls with OpenTelemetry.
What you get
- The span is classified as a tool step in trace views and per-kind analytics.
- The tool’s arguments and result render as its input and output, searchable on
input.valueandoutput.value. - The tool name, call id and type are stored on the span as you sent them, so you can tie an execution back to the model’s request that triggered it.
- Failed tool runs carry
error.typeand an exception event, so they appear in error analysis. - With
mcp.method.nameset, MCP tools are counted separately in context attribution.
A tool span is not counted as an LLM call, even if it carries a model. The tool span needs no cost attribute, and you should never send one. Rius computes cost on the model call spans from the model name and token counts.
When something else fits
- If you only need the model’s tool request recorded, put it in the
tool_callpart ofgen_ai.output.messageson the LLM span and skip a separate tool span. - If you want conveniences such as message normalization and a raised attribute limit, use the Rius SDK instead of hand-written spans. Plain OpenTelemetry stays fully supported either way.