---
title: "OpenTelemetry for AI agents: a local OTLP receiver"
description: "Point the Vercel AI SDK, Claude Agent SDK, OpenAI Agents SDK, LangGraph or any OTLP exporter at the local receiver."
canonical: https://coding.sh/docs/otel
human_url: https://coding.sh/docs/otel
markdown_url: https://coding.sh/docs/otel.md
agents_url: https://coding.sh/agents.md
llms_url: https://coding.sh/llms.txt
updated: 2026-11-11
---

# 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](https://coding.sh/docs/how-to/trace-your-agent).

## Start the receiver

shell

```
coding.sh collector
```

It runs in the foreground. The environment for an exporter goes to stdout; status goes to stderr.

coding.sh collector

```
$ 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-protobuf` or `application/json`, optionally gzip, deflate or br encoded.
- It binds to 127.0.0.1. `--host` changes the interface and `--port 0` picks 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.

shell

```
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:

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

```
// 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
```

typescript

```
// 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());
```

typescript

```
// 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:

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:

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

python

```
# 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)
```

shell

```
# 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

shell

```
# 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.
```

python

```
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](https://github.com/vercel/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](https://coding.sh/docs/storage#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](https://github.com/jackulau/coding.sh/blob/main/docs/PRODUCTION.md#19-forward-and-sinks).

To set coding.sh up for a user, follow the agent workflow at [/agents.md](https://coding.sh/agents.md).
