Record any agent with OpenTelemetry
Point the Vercel AI SDK, Claude Agent SDK, OpenAI Agents SDK, LangGraph or any OTLP exporter at the local receiver.
coding.sh collector (alias otel) runs a local OTLP/HTTP receiver. Anything that exports OpenTelemetry traces
or logs can send to it, and each run shows up as a session next to your coding-agent sessions.
For a step-by-step path that also covers the SDKs and evals, see Trace your own agent.
Start the receiver
coding.sh collector It runs in the foreground. The environment for an exporter goes to stdout; status goes to stderr.
$ coding.sh collector export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf export OTEL_TRACES_EXPORTER=otlp export OTEL_LOGS_EXPORTER=otlp export CLAUDE_CODE_ENABLE_TELEMETRY=1 Ready on http://127.0.0.1:4318 Press Ctrl-C to stop
- Routes:
POST /v1/traces,/v1/logs(mapped to events) and/v1/metrics(accepted, then dropped). - Bodies:
application/x-protobuforapplication/json, optionally gzip, deflate or br encoded. - It binds to 127.0.0.1.
--hostchanges the interface and--port 0picks any free port. - Secrets are redacted before anything is stored, the same as for hook events.
The receiver reads OpenTelemetry GenAI conventions (gen_ai.*), OpenInference (openinference.span.kind,
llm.*, tool.*), the Vercel AI SDK (ai.*), and Claude Code, Codex and older Gemini CLI log events
(claude_code.api_request and similar).
Claude Code and the Claude Agent SDK
The Agent SDK inherits Claude Code's environment variables.
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json # or http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
export OTEL_LOG_USER_PROMPTS=1 # prompt text (session titles); off by default
export OTEL_LOG_TOOL_DETAILS=1 # tool_parameters (Bash commands, file paths)
export OTEL_LOGS_EXPORT_INTERVAL=1000 # ms, default 5000
# optional, beta: spans too. Model/tool calls seen in both logs and spans
# are de-duplicated by request_id / tool_use_id.
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 OTEL_TRACES_EXPORTER=otlp Agent SDK in TypeScript:
// options.env replaces the environment, so spread process.env into it
query({
prompt,
options: {
env: {
...process.env,
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
OTEL_LOGS_EXPORTER: "otlp",
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4318",
},
},
});
In Python, ClaudeAgentOptions(env={...}) takes the same keys and is merged over os.environ. Don't use the
console exporter under the SDK: it corrupts the SDK's stdout channel.
Vercel AI SDK
// instrumentation.ts
// npm i @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
const sdk = new NodeSDK({
serviceName: "my-agent",
traceExporter: new OTLPTraceExporter({ url: "http://127.0.0.1:4318/v1/traces" }),
});
sdk.start(); // short scripts: await sdk.shutdown() before exit to flush // AI SDK 6 and earlier: per call
await generateText({
model,
prompt,
experimental_telemetry: {
isEnabled: true,
functionId: "my-agent",
metadata: { sessionId: "chat-123" },
},
});
// AI SDK 7: once at startup (npm i @ai-sdk/otel); the per-call option is `telemetry`
import { registerTelemetry } from "ai";
import { OpenTelemetry } from "@ai-sdk/otel";
registerTelemetry(new OpenTelemetry()); // Next.js: registerOTel from @vercel/otel, with the endpoint in the environment
// OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
registerOTel({ serviceName: "my-agent" }); OpenAI Agents SDK
Through OpenInference. Python:
# pip install openai-agents openinference-instrumentation-openai-agents \
# opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider(resource=Resource.create({"service.name": "my-agent"}))
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://127.0.0.1:4318/v1/traces"))
)
OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)
# OPENAI_AGENTS_DISABLE_TRACING=1 also stops uploads to OpenAI's tracing backend. TypeScript:
// npm i @arizeai/openinference-instrumentation-openai-agents \
// @opentelemetry/exporter-trace-otlp-proto
// Register a NodeTracerProvider with
// new BatchSpanProcessor(new OTLPTraceExporter({ url: "http://127.0.0.1:4318/v1/traces" }))
// then:
new OpenAIAgentsInstrumentation({ tracerProvider }).manuallyInstrument(agents); LangChain and LangGraph
# OpenInference: pip install openinference-instrumentation-langchain
# (plus the TracerProvider / OTLPSpanExporter setup from the OpenAI Agents SDK example)
from openinference.instrumentation.langchain import LangChainInstrumentor
LangChainInstrumentor().instrument(tracer_provider=provider) # or LangSmith's OTel exporter: pip install "langsmith[otel]"
export LANGSMITH_OTEL_ENABLED=true LANGSMITH_TRACING=true LANGSMITH_OTEL_ONLY=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 OTEL_SERVICE_NAME=my-agent
# LangGraph thread_id metadata (langsmith.metadata.thread_id) groups traces into one session. Any Python app
# pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 # base URL: the SDK appends /v1/traces
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # the Python HTTP exporter speaks protobuf only
export OTEL_SERVICE_NAME=my-agent # becomes the agent id (kebab-cased)
# A signal-specific OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is used verbatim: include /v1/traces. 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
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
# logs: from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter Group traces into one session
One trace is one session. Set gen_ai.conversation.id or session.id on spans (AI SDK:
metadata.sessionId), and every trace with that id becomes a turn of one session. With OpenInference:
with using_session("chat-123"):.
Tag eval runs with OTEL_RESOURCE_ATTRIBUTES=coding_sh.run_id=<id>.
How spans become sessions
Spans from eve agents are recognized and enriched.
Model-call spans become llm.call events with duration, model and token usage. Tool spans become
tool.start/tool.end. Nested agent spans become subagents with their own child session. Usage is read only from
model-call spans, because agent spans usually repeat their children's totals.
Spans arrive out of order, so after each batch the touched sessions are rebuilt from all their events. Event ids derive from the trace and span ids, so exporter retries insert nothing.
| Attribute | What it sets |
|---|---|
gen_ai.usage.reasoning.output_tokens, AI SDK reasoningTokens, Codex reasoning_token_count, Gemini CLI thoughts_token_count | payload.reasoningTokens on the llm.call. Part of the output tokens, so totals do not change. |
gen_ai.skill.name on a tool span | payload.skill, with skillSource and skillResource. |
gen_ai.skill.script.exit_code | exitCode on tool.end. |
Claude Code skill_activated, Codex codex.skill_invocation | The skill, recorded as invoked. |
gen_ai.conversation.compacted | One context.compact event per session. |
Claude Code compaction | A context.compact event with tokensBefore, tokensAfter, ok and the duration. |
gen_ai.main_agent.name on the resource | The agent name, ahead of service.name. |
Claude Code agent_id, parent_agent_id | Tags a subagent's events. The parent is kept as payload.parentAgentId. |
Codex codex.agent_response | Links a subagent's thread to the parent thread (parent.conversation.id). |
Each skill becomes a skill:<name> tag on the session. Placeholder names such as custom_skill are skipped.
A reported cache read or write of 0 is left out.
Forward and sinks
Configured sinks mirror events received over OTLP, in both coding.sh collector and
coding.sh serve. coding.sh serve can also forward every trace it commits to another OTLP collector: set
CODING_SH_FORWARD_URL. See
Forward and sinks.