Skip to content

Instantly share code, notes, and snippets.

@sagikazarmark
Created April 22, 2026 08:17
Show Gist options
  • Select an option

  • Save sagikazarmark/ecc70619d4cbaeb456e3167c4dd96e89 to your computer and use it in GitHub Desktop.

Select an option

Save sagikazarmark/ecc70619d4cbaeb456e3167c4dd96e89 to your computer and use it in GitHub Desktop.
Claude Code OpenInference OTEL connector config
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
# Drop traces from Docker Compose itself (it picks up OTEL_EXPORTER_OTLP_ENDPOINT
# from the environment and sends its own cli/logs, cli/ps, HEAD /_ping traces)
filter:
error_mode: ignore
traces:
span:
- 'resource.attributes["service.name"] == "compose"'
transform:
trace_statements:
- context: span
statements:
## ─── Claude Code → OpenInference ───────────────────────────
# Span kind mapping
- set(attributes["openinference.span.kind"], "LLM") where attributes["span.type"] == "llm_request"
- set(attributes["openinference.span.kind"], "AGENT") where attributes["span.type"] == "interaction"
- set(attributes["openinference.span.kind"], "TOOL") where attributes["span.type"] == "tool"
- set(attributes["openinference.span.kind"], "TOOL") where attributes["span.type"] == "tool.execution"
- set(attributes["openinference.span.kind"], "CHAIN") where attributes["span.type"] == "tool.blocked_on_user"
- set(attributes["openinference.span.kind"], "CHAIN") where attributes["span.type"] == "hook"
# LLM attributes
- set(attributes["llm.model_name"], attributes["model"]) where attributes["span.type"] == "llm_request"
- set(attributes["llm.token_count.prompt"], attributes["input_tokens"]) where attributes["span.type"] == "llm_request"
- set(attributes["llm.token_count.completion"], attributes["output_tokens"]) where attributes["span.type"] == "llm_request"
# Cache token counts
- set(attributes["llm.token_count.cache_read"], attributes["cache_read_tokens"]) where attributes["span.type"] == "llm_request"
- set(attributes["llm.token_count.cache_creation"], attributes["cache_creation_tokens"]) where attributes["span.type"] == "llm_request"
# Tool attributes
- set(attributes["tool.name"], attributes["tool_name"]) where attributes["span.type"] == "tool"
- set(attributes["input.value"], attributes["full_command"]) where attributes["span.type"] == "tool" and attributes["full_command"] != nil
- set(attributes["input.value"], attributes["file_path"]) where attributes["span.type"] == "tool" and attributes["file_path"] != nil and attributes["full_command"] == nil
# Interaction input (if user_prompt is present via OTEL_LOG_USER_PROMPTS=1)
- set(attributes["input.value"], attributes["user_prompt"]) where attributes["span.type"] == "interaction"
# Set span status from success attribute (Claude Code leaves status Unset)
- set(status.code, 1) where attributes["success"] == true
- set(status.code, 2) where attributes["success"] == false
- set(status.message, attributes["error"]) where attributes["success"] == false and attributes["error"] != nil
## ─── OTel GenAI semconv (gen_ai.*) → OpenInference ─────────
## For agents that use standard gen_ai.* attributes
# Span kind
- set(attributes["openinference.span.kind"], "LLM") where attributes["gen_ai.system"] != nil
# Model
- set(attributes["llm.model_name"], attributes["gen_ai.request.model"]) where attributes["gen_ai.request.model"] != nil
# Token counts
- set(attributes["llm.token_count.prompt"], attributes["gen_ai.usage.input_tokens"]) where attributes["gen_ai.usage.input_tokens"] != nil
- set(attributes["llm.token_count.completion"], attributes["gen_ai.usage.output_tokens"]) where attributes["gen_ai.usage.output_tokens"] != nil
## ─── OpenCode / Vercel AI SDK (ai.*) → OpenInference ───────
## OpenCode uses Vercel AI SDK which emits ai.* attributes
# Span kind mapping by span name
- set(attributes["openinference.span.kind"], "LLM") where name == "ai.streamText"
- set(attributes["openinference.span.kind"], "LLM") where name == "ai.streamText.doStream"
- set(attributes["openinference.span.kind"], "LLM") where name == "ai.generateText"
- set(attributes["openinference.span.kind"], "LLM") where name == "ai.generateText.doGenerate"
- set(attributes["openinference.span.kind"], "TOOL") where name == "ai.toolCall"
# Model
- set(attributes["llm.model_name"], attributes["ai.model.id"]) where attributes["ai.model.id"] != nil
- set(attributes["llm.model_name"], attributes["ai.model.provider"]) where attributes["ai.model.provider"] != nil and attributes["ai.model.id"] == nil
# Token counts
- set(attributes["llm.token_count.prompt"], attributes["ai.usage.promptTokens"]) where attributes["ai.usage.promptTokens"] != nil
- set(attributes["llm.token_count.completion"], attributes["ai.usage.completionTokens"]) where attributes["ai.usage.completionTokens"] != nil
# LLM input/output content
- set(attributes["input.value"], attributes["ai.prompt.messages"]) where attributes["ai.prompt.messages"] != nil
- set(attributes["output.value"], attributes["ai.response.text"]) where attributes["ai.response.text"] != nil
# Tool attributes
- set(attributes["tool.name"], attributes["ai.toolCall.name"]) where attributes["ai.toolCall.name"] != nil
- set(attributes["input.value"], attributes["ai.toolCall.args"]) where attributes["ai.toolCall.args"] != nil
- set(attributes["output.value"], attributes["ai.toolCall.result"]) where attributes["ai.toolCall.result"] != nil
# Tool definitions (for context in Phoenix detail view)
- set(attributes["tool.parameters"], attributes["ai.prompt.tools"]) where attributes["ai.prompt.tools"] != nil
## ─── Codex CLI (codex.*) → OpenInference ───────────────────
## Codex uses custom codex.* attributes. Most data is in log
## events, not trace spans — these cover what lands on spans.
## UNVERIFIED: needs validation with debug exporter + real Codex traces.
# Session/exec spans
- set(attributes["openinference.span.kind"], "AGENT") where name == "codex.session"
- set(attributes["openinference.span.kind"], "AGENT") where name == "codex.exec"
# Model (common attribute on codex events/spans)
- set(attributes["llm.model_name"], attributes["model"]) where resource.attributes["service.name"] == "codex-cli" and attributes["model"] != nil
# Token counts (from codex.sse_event spans/events)
- set(attributes["llm.token_count.prompt"], attributes["input_token_count"]) where attributes["input_token_count"] != nil
- set(attributes["llm.token_count.completion"], attributes["output_token_count"]) where attributes["output_token_count"] != nil
- set(attributes["llm.token_count.cache_read"], attributes["cached_token_count"]) where attributes["cached_token_count"] != nil
# Tool results
- set(attributes["tool.name"], attributes["tool_name"]) where resource.attributes["service.name"] == "codex-cli" and attributes["tool_name"] != nil
- set(attributes["input.value"], attributes["arguments"]) where resource.attributes["service.name"] == "codex-cli" and attributes["arguments"] != nil
- set(attributes["output.value"], attributes["output"]) where resource.attributes["service.name"] == "codex-cli" and attributes["output"] != nil
# Prompt content (requires otel.log_user_prompt = true in codex config)
- set(attributes["input.value"], attributes["prompt"]) where resource.attributes["service.name"] == "codex-cli" and attributes["prompt"] != nil
# Status
- set(status.code, 1) where resource.attributes["service.name"] == "codex-cli" and attributes["success"] == true
- set(status.code, 2) where resource.attributes["service.name"] == "codex-cli" and attributes["success"] == false
# Promote SpanEvent content to span attributes so Phoenix can display them
- context: spanevent
statements:
- set(span.attributes["output.value"], attributes["output"]) where name == "tool.output" and attributes["output"] != nil
- set(span.attributes["output.value"], attributes["diff"]) where name == "tool.output" and attributes["diff"] != nil and attributes["output"] == nil
# Transform all agent formats → gen_ai.* semconv for Langfuse
transform/genai:
trace_statements:
- context: span
statements:
## ─── Claude Code → gen_ai.* ────────────────────────────────
- set(attributes["gen_ai.system"], "anthropic") where attributes["span.type"] == "llm_request"
- set(attributes["gen_ai.request.model"], attributes["model"]) where attributes["span.type"] == "llm_request"
- set(attributes["gen_ai.usage.input_tokens"], attributes["input_tokens"]) where attributes["span.type"] == "llm_request"
- set(attributes["gen_ai.usage.output_tokens"], attributes["output_tokens"]) where attributes["span.type"] == "llm_request"
## ─── OpenCode / Vercel AI SDK → gen_ai.* ───────────────────
- set(attributes["gen_ai.system"], "openai") where attributes["ai.model.provider"] != nil
- set(attributes["gen_ai.request.model"], attributes["ai.model.id"]) where attributes["ai.model.id"] != nil
- set(attributes["gen_ai.usage.input_tokens"], attributes["ai.usage.promptTokens"]) where attributes["ai.usage.promptTokens"] != nil
- set(attributes["gen_ai.usage.output_tokens"], attributes["ai.usage.completionTokens"]) where attributes["ai.usage.completionTokens"] != nil
## ─── Codex CLI → gen_ai.* ──────────────────────────────────
- set(attributes["gen_ai.system"], "openai") where resource.attributes["service.name"] == "codex-cli" and attributes["model"] != nil
- set(attributes["gen_ai.request.model"], attributes["model"]) where resource.attributes["service.name"] == "codex-cli" and attributes["model"] != nil
- set(attributes["gen_ai.usage.input_tokens"], attributes["input_token_count"]) where attributes["input_token_count"] != nil
- set(attributes["gen_ai.usage.output_tokens"], attributes["output_token_count"]) where attributes["output_token_count"] != nil
# gen_ai.* passthrough — already correct, no transform needed
exporters:
otlp/phoenix:
endpoint: phoenix:4317
tls:
insecure: true
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
# Langfuse: OTLP HTTP only, requires Basic Auth
otlphttp/langfuse:
endpoint: http://langfuse-web:3000/api/public/otel
headers:
Authorization: "Basic cGstbGYtYWlvMTF5OnNrLWxmLWFpbzExeQ=="
debug:
verbosity: detailed
service:
pipelines:
# Phoenix: transformed to OpenInference
traces/phoenix:
receivers: [otlp]
processors: [filter, transform]
exporters: [otlp/phoenix, debug]
# Tempo: raw traces, original attributes preserved
traces/tempo:
receivers: [otlp]
processors: [filter]
exporters: [otlp/tempo]
# Langfuse: transformed to gen_ai.* semconv
traces/langfuse:
receivers: [otlp]
processors: [filter, transform/genai]
exporters: [otlphttp/langfuse]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment