Skip to content

Tracing (OpenTelemetry)

Distributed tracing lets you follow a single request as it fans out across services, spans, and asynchronous work. Each hop in that request emits a span – a timed record that captures what happened, how long it took, and any data you attach. When spans share a trace context, your observability backend can stitch them into a timeline that exposes latency hot spots, errors, and the path taken through your system.

All agent libraries emit OpenTelemetry spans that follow the Semantic Conventions for Generative AI Systems. You get high‑level visibility into the agent run itself, every tool invocation, and the underlying language model call made through the SDK. Because we rely on the standard OpenTelemetry APIs, any additional spans you create inside tool implementations (or downstream microservices they call) automatically participate in the same trace thanks to distributed tracing.

Each run produces the spans below. Names and attributes are consistent across TypeScript, Rust, and Go.

Span nameWhat it coversKey attributes
llm_agent.runEntire non-streaming run, from invocation to completiongen_ai.operation.name=invoke_agent, gen_ai.agent.name, aggregated gen_ai.model.* usage tokens, llm_agent.cost
llm_agent.run_streamEntire streaming run (start → final response/error)Same attributes as llm_agent.run; emitted once the stream finishes or terminates with an error
llm_agent.toolEach tool execute call (success or failure)gen_ai.operation.name=execute_tool, gen_ai.tool.call.id, gen_ai.tool.name, gen_ai.tool.description, gen_ai.tool.type=function, exception metadata when execute throws

Tool spans wrap the entire execute handler, so any tracing you add inside runs as a child. If a tool calls out to other services that already emit telemetry, their spans join the same trace because the OpenTelemetry context is active for the duration of the tool call.

The SDK instruments all language model calls. Whenever an agent (or your own application) calls a LanguageModel, you will see:

Span nameWhat it coversKey attributes (subset)
llm_sdk.generateFull synchronous generate call (start → finish/error)gen_ai.operation.name=generate_content, gen_ai.provider.name, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, llm_sdk.cost, gen_ai.request.temperature, gen_ai.request.max_tokens, gen_ai.request.top_p, gen_ai.request.top_k, gen_ai.request.presence_penalty, gen_ai.request.frequency_penalty, gen_ai.request.seed
llm_sdk.streamComplete lifetime of stream (initial call + drain)Same attributes as generate plus gen_ai.server.time_to_first_token and incremental cost/usage tallied from partials

If an error bubbles up from the provider, the span status is set to ERROR and the exception is recorded, making it easy to spot call failures in tracing UIs.

Tracing shown on Jaeger UI

The examples below show identical agents across TS, Rust, and Go. Each run checks the weather for Seattle, then sends it to a contact and produces spans beneath the agent root span.

examples/tracing.ts
import {
Agent,
tool,
type AgentItem,
type AgentResponse,
type AgentToolResult,
} from "@hoangvvo/llm-agent";
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { getModel } from "./get-model.ts";
const provider = new NodeTracerProvider({
resource: resourceFromAttributes({
"service.name": "agent-js-tracing-example",
}),
spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())],
});
provider.register();
// We'll use this tracer inside tool implementations for nested spans.
const tracer = trace.getTracer("examples/agent-js/tracing");
interface AgentContext {
customer_name: string;
}
const model = getModel("openai", "gpt-4o-mini");
const getWeatherTool = tool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "City to get the weather for" },
},
required: ["city"],
additionalProperties: false,
},
async execute({ city }: { city: string }) {
return tracer.startActiveSpan(
"tools.get_weather",
async (span): Promise<AgentToolResult> => {
try {
// Record the city lookup while simulating work.
span.setAttribute("weather.city", city);
await new Promise((resolve) => setTimeout(resolve, 100));
return {
content: [
{
type: "text",
text: JSON.stringify({
city,
forecast: "Sunny",
temperatureC: 24,
}),
},
],
is_error: false,
};
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: String(error),
});
throw error;
} finally {
span.end();
}
},
);
},
});
const notifyContactTool = tool({
name: "send_notification",
description: "Send a text message to a recipient",
parameters: {
type: "object",
properties: {
phone_number: { type: "string" },
message: { type: "string" },
},
required: ["phone_number", "message"],
additionalProperties: false,
},
async execute({
phone_number,
message,
}: {
phone_number: string;
message: string;
}) {
return tracer.startActiveSpan(
"tools.send_notification",
async (span): Promise<AgentToolResult> => {
try {
// Capture metadata about the outbound notification.
span.setAttribute("notification.phone", phone_number);
span.setAttribute("notification.message_length", message.length);
await new Promise((resolve) => setTimeout(resolve, 80));
return {
content: [
{
type: "text",
text: JSON.stringify({ status: "sent", phone_number, message }),
},
],
is_error: false,
};
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: String(error),
});
throw error;
} finally {
span.end();
}
},
);
},
});
const agent = new Agent<AgentContext>({
name: "Trace Assistant",
model,
instructions: [
// Keep these instructions aligned with the Rust/Go tracing examples.
"Coordinate weather updates and notifications for clients.",
"When a request needs both a forecast and a notification, call get_weather before send_notification and summarize the tool results in your reply.",
({ customer_name }) =>
`When asked to contact someone, include a friendly note from ${customer_name}.`,
],
tools: [getWeatherTool, notifyContactTool],
});
// Single-turn request that forces both tools to run.
const items: AgentItem[] = [
{
type: "message",
role: "user",
content: [
{
type: "text",
text: "Please check the weather for Seattle today and text Mia at +1-555-0100 with the summary.",
},
],
},
];
const response: AgentResponse = await agent.run({
context: { customer_name: "Skyline Tours" },
input: items,
});
console.log(JSON.stringify(response.content, null, 2));
await provider.forceFlush();
await provider.shutdown();