How do I record tool calls with the OpenTelemetry JavaScript SDK?
Rius records a tool call from the OpenTelemetry JavaScript SDK when you add a span with gen_ai.operation.name set to execute_tool and gen_ai.tool.name set to the tool’s name. The SDK does not add these attributes for you, so you set them on the span in your tool function. Rius then shows the span as a tool step, and the arguments and result you attach appear as the span’s input and output.
How it works
The OpenTelemetry JavaScript SDK sends whatever attributes you put on a span, and Rius reads them. The exporter setup only decides where spans go. It does not mark any span as a tool call, so each tool function needs its own span.
These are the settings a tool span needs:
gen_ai.operation.nameset toexecute_tool. This is what marks the span as a tool call. You can setopeninference.span.kindtoTOOLinstead, and Rius derives the other one. A span with neither is not treated as a tool call.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. It lets you match the tool span to the tool call in the model’s output.gen_ai.tool.type, such asfunction,extensionordatastore.gen_ai.agent.name, the agent executing the tool.gen_ai.tool.call.argumentsandgen_ai.tool.call.result, the input and the result. Rius moves them toinput.valueandoutput.value, so search on those two keys.error.type, a low-cardinality error class such astool_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.
Because execute_tool maps to the TOOL kind, the span appears under the Tool kind in the Spans tab’s All kinds menu. The Tool Loop alert counts only spans of kind TOOL, so a tool span without that kind is never counted.
On the resource side, set service.name through OTEL_SERVICE_NAME. Every trace view groups by it. To name your agent, set gen_ai.main_agent.name as a resource attribute. Without the Rius SDK, Rius does not set it for you.
Set it up
- Register the Node.js SDK once, with the protobuf exporter and no URL or headers in code, so it reads the
OTEL_EXPORTER_OTLP_*variables. Those variables take effect through the SDK, not on their own. The full environment variable set is on Vanilla OpenTelemetry SDKs.
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
const sdk = new NodeSDK({
// No url or headers in code, so the exporter reads OTEL_EXPORTER_OTLP_*
traceExporter: new OTLPTraceExporter(),
// Rius ingests traces only
metricReaders: [],
logRecordProcessors: [],
})
sdk.start()
// ... your agent code ...
// Flushes queued spans before the process exits (top-level await needs an ES module)
await sdk.shutdown()- Wrap each tool function in a span that carries the tool attributes. The names
getWeather,lookupWeatherandget_weatherbelow are examples from your own code. Replace them with yours.
import { SpanStatusCode, trace } from '@opentelemetry/api'
const tracer = trace.getTracer('my-agent')
async function getWeather(city: string, callId: string) {
const span = tracer.startSpan('execute_tool get_weather')
span.setAttributes({
'gen_ai.operation.name': 'execute_tool',
'gen_ai.tool.name': 'get_weather',
'gen_ai.tool.call.id': callId,
'gen_ai.tool.type': 'function',
'gen_ai.tool.call.arguments': JSON.stringify({ city }),
})
try {
const result = await lookupWeather(city)
span.setAttributes({ 'gen_ai.tool.call.result': JSON.stringify(result) })
return result
} catch (err) {
span.setAttributes({ 'error.type': 'tool_error' })
span.setStatus({ code: SpanStatusCode.ERROR })
throw err
} finally {
span.end()
}
}-
Call the tool inside your agent run’s span. Nest spans the normal OpenTelemetry way, through the parent context, and they arrive as one trace. Wrap each agent run in a single root span, because that root’s name, duration and status are what run-level views show.
-
If the tool is called over the Model Context Protocol, also set
mcp.method.nametotools/callon the span. Rius uses it to tell MCP tools from local ones when it reports how many context tokens MCP tools took. You can also setmcp.protocol.version. -
Run the agent once, open Traces in the console, and open the trace. The tool span should show under the Tool kind. The Span attribute reference lists every attribute Rius reads.
What you get
- Each tool call appears as its own span in the trace, nested under the step that called it, with the tool name, its arguments and its result.
- Errors in a tool carry
error.typeand an error status, so they surface in error analysis. - Tool spans are filterable by kind, and they are the spans the Tool Loop alert counts.
- Tool spans carry no tokens or cost. Those sit on the span for each model call, where Rius computes cost from the model and token counts. Costs are list-price estimates in US dollars.
- Your spans reach Rius with the same attribute names the Rius SDKs use, so moving to a Rius SDK later does not change what you see. The end-to-end path is on Trace LLM calls with OpenTelemetry.
When something else fits
- If you use a library or framework the Rius SDK integrates with, such as MCP, the integration already records tool calls, and hand-written spans would only duplicate them.
- If you want the SDK to set up the tracer provider and exporter, use the Rius TypeScript SDK instead of building your own OpenTelemetry setup.