Trazas de Claude Code en Jaeger: dónde se va cada minuto
Una traza de Claude Code es el desglose temporal de un prompt: un span raíz por cada mensaje tuyo y, colgando de él, cada llamada al modelo, cada herramienta y cada espera de permiso, con su duración. Si lo envías a Jaeger en local, en unos minutos sabes si una sesión lenta se debió al modelo, a tus tests o a ti.
Qué responde una traza que /usage no responde
/usage y la statusline te dicen cuánto gastaste; una traza te dice en qué orden y cuánto tardó cada pieza. La pregunta típica es: "le pedí arreglar un test, tardó 14 minutos, ¿por qué?". Con contadores agregados no hay respuesta. Con una cascada de spans ves si fueron diez llamadas al modelo, un pytest de seis minutos o un diálogo de permiso que nadie contestó.
Según la documentación de monitorización, el tracing está en beta y desactivado por defecto. Cada prompt genera esta jerarquía:
claude_code.interaction
├── claude_code.llm_request
├── claude_code.hook (solo con tracing detallado beta)
└── claude_code.tool
├── claude_code.tool.blocked_on_user
├── claude_code.tool.execution
└── (herramienta Agent) spans llm_request / tool del subagenteEl tema tiene actualidad: el changelog oficial recoge que la v2.1.300 (14/10/2026) añadió a los spans atributos de las convenciones semánticas GenAI de OpenTelemetry, como gen_ai.provider.name, gen_ai.conversation.id, gen_ai.tool.name y error.type. Así, un backend que ya entiende esas convenciones puede agrupar las trazas sin que tengas que mapear nada a mano. La v2.1.301 (15/10/2026) añadió classifier_verdict y permission_mode a los eventos tool_decision, pero esos van por el canal de logs, no por el de trazas.
Paso 1: levantar Jaeger en local con Docker
Para empezar no necesitas un Collector ni Grafana: Jaeger acepta OTLP directamente. Este comando, sacado de la guía oficial de Jaeger (versión 2.22 en la fecha de publicación), arranca la imagen all-in-one con almacenamiento en memoria:
docker run --rm --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
cr.jaegertracing.io/jaegertracing/jaeger:2.22.0El puerto 16686 es la interfaz web, el 4317 recibe OTLP por gRPC y el 4318 OTLP por HTTP. La guía publica también los puertos 5778 y 9411, que aquí no hacen falta. Como el almacenamiento es en memoria, las trazas se pierden al parar el contenedor. Para depurar está bien; para guardar histórico, no.
Paso 2: activar las trazas en Claude Code
Hacen falta dos interruptores, un exporter y un protocolo explícito. La documentación indica que las trazas requieren CLAUDE_CODE_ENABLE_TELEMETRY=1 y además CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1. El protocolo OTLP no tiene valor por defecto, así que tienes que fijarlo. Abre una terminal nueva y lanza Claude Code desde ella:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_TRACES_EXPORT_INTERVAL=1000
claudeEl intervalo por defecto para enviar lotes de spans es de 5000 ms. Bajarlo a 1000 sirve para ver las trazas casi al momento mientras depuras. Tres detalles de la documentación que conviene saber antes de seguir:
- El
.claude/settings.jsondel repositorio no puede activar la telemetría ni fijar el destino del exporter. Usa la shell, tus settings de usuario o los settings gestionados. Así, un repo clonado no puede mandar tus datos a un servidor ajeno. - Las variables
OTEL_*no pasan a los subprocesos (Bash, hooks, servidores MCP). Si tus tests también emiten OpenTelemetry, no van a heredar esta configuración. - Los subprocesos sí heredan
TRACEPARENT, de modo que una herramienta tuya instrumentada puede colgar sus spans dentro de la traza del agente.
Paso 3: generar una traza y encontrarla
Cada prompt que envías es una traza, y el servicio se llama claude-code. En un repositorio con tests, pide algo que obligue a usar herramientas, por ejemplo: "ejecuta los tests, localiza el que falla y propón un arreglo sin aplicarlo". Acepta los permisos cuando te los pida.
Abre http://localhost:16686, elige el servicio claude-code en el desplegable y pulsa "Find Traces". Según la sección "Service information" de la documentación, las sesiones locales lanzadas desde la pestaña Code de la app de escritorio salen como claude-code-desktop. Si filtras por servicio, incluye los dos.
Lo esperable es una traza por prompt con raíz claude_code.interaction. Su atributo interaction.duration_ms es el tiempo total que esperaste. Por defecto, el texto del prompt no aparece: el span solo guarda user_prompt_length.
Cómo leer la cascada: qué span mirar y qué decidir
Busca la barra más larga y mira de qué tipo es: cada tipo apunta a una causa y a un arreglo distinto. Esta tabla usa los atributos que la documentación lista para cada span:
| Span dominante | Atributos que mirar | Qué suele significar | Qué hacer |
|---|---|---|---|
tool.blocked_on_user | duration_ms, decision, source | El agente no era lento: estaba esperando tu permiso. | Añade reglas de permiso para los comandos de solo lectura que siempre apruebas. |
tool.execution de Bash | duration_ms, success y, en el padre claude_code.tool, bash_argv0 y bash_command_class | El tiempo se va en tu suite de tests o en tu build. | Indica en CLAUDE.md cómo lanzar solo los tests afectados. |
Muchos llm_request cortos | response.has_tool_call, stop_reason | Bucle de exploración: lee archivo por archivo. | Dale rutas concretas en el prompt o un mapa del repo. |
llm_request con attempt mayor que 1 | status_code, error_class | Reintentos por errores 429 o 529, no problemas de tu código. | Espera o revisa límites. Cambiar el prompt no lo arregla. |
llm_request con ttft_ms alto | input_tokens, cache_read_tokens, cache_creation_tokens | Contexto grande que se reescribe en caché en vez de leerse de ella. | Revisa qué invalidó la caché antes de pensar en cambiar de modelo. |
| Spans bajo la herramienta Agent | subagent_type (requiere OTEL_LOG_TOOL_DETAILS=1) | Un subagente concreto concentra el tiempo. | Acota su tarea o su modelo en la definición del subagente. |
Un consejo práctico, que es heurística y no dato: antes de tocar nada, compara la suma de blocked_on_user con la de llm_request. En sesiones interactivas con permisos estrictos, lo primero suele explicar más de lo que esperas.
Si Jaeger no muestra nada
Comprueba antes que Claude Code genera spans, y después que llegan al backend. Sigue este orden:
- Exporter de consola. Cambia a
OTEL_TRACES_EXPORTER=console. Si no ves spans en la salida, faltaCLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1o lanzasteclaudedesde otra terminal sin las variables. - Protocolo y puerto.
grpcva al 4317. Si usashttp/protobuf, el endpoint es el 4318. Mezclarlos es el error más habitual. - Log de depuración. La documentación recomienda arrancar con
claude --debug-file /tmp/claude-otel.logy buscar errores de exportación con la marca[3P telemetry]. - Contenedor vivo.
docker psdebe listarjaeger. Con--rm, al pararlo pierdes todo.
Privacidad y cuándo no merece la pena montarlo
Por defecto los spans omiten el texto del prompt, los argumentos de las herramientas y su salida, y conviene que siga así salvo que el backend sea tuyo. Para incluirlos, la documentación describe tres variables. OTEL_LOG_USER_PROMPTS=1 añade el prompt. OTEL_LOG_TOOL_DETAILS=1 añade rutas de archivo y comandos completos. OTEL_LOG_TOOL_CONTENT=1 añade un evento tool.output con contenido de archivos y salida de Bash, truncado a 60 KB por atributo por defecto. Con las tres activas, tu backend guarda código y posibles secretos. En un Jaeger local y efímero el riesgo es bajo. En un backend compartido, solo debes activarlas si la política de datos lo permite.
No te compensa montarlo en estos casos:
- Solo te interesa el coste total. Las métricas
claude_code.cost.usageyclaude_code.token.usage, o el propio /usage, ya te lo dan sin trazas. - Quieres auditar permisos denegados.
classifier_verdictysourceestán en el eventotool_decision, que va porOTEL_LOGS_EXPORTER. Jaeger solo guarda trazas, así que necesitas un backend de logs. - Quieres medir tus hooks. El span
claude_code.hookexige tracing detallado beta (ENABLE_BETA_TRACING_DETAILED=1yBETA_TRACING_ENDPOINT). En sesiones interactivas, además, tu organización tiene que estar en una allowlist. - Dependes de nombres de atributos estables. El tracing está en beta y el changelog lo modifica casi cada semana. No construyas alertas de producción sobre él sin fijar la versión.
Preguntas frecuentes
¿Las trazas se envían a Anthropic?
No. Según la documentación, la exportación OpenTelemetry es opt-in y solo va al endpoint que tú configures. Anthropic tiene su propia telemetría operativa, que se gestiona aparte.
¿Puedo dejarlo activado siempre?
Sí, con el contenido redactado y en tus settings de usuario. Eso sí, con Jaeger en memoria no guardas nada entre reinicios. Si quieres histórico, apunta el endpoint a un Collector o a un backend con almacenamiento persistente y devuelve OTEL_TRACES_EXPORT_INTERVAL a su valor por defecto.
¿Funciona con Claude Code en Bedrock o Vertex?
La documentación indica que con una API key directa, Bedrock, Agent Platform de Google Cloud o Foundry no hay cuenta de Claude en la sesión. En ese caso solo se rellenan user.id y session.id, y la identidad del usuario tienes que añadirla tú con OTEL_RESOURCE_ATTRIBUTES.