Trazas de Claude Code en Jaeger: dónde se va cada minuto

Cascada abstracta de barras horizontales de distintas longitudes y colores que se ramifican desde una barra raíz sobre un fondo oscuro, como un diagrama de tiempos

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 subagente

El 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.0

El 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
claude

El 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.json del 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 dominanteAtributos que mirarQué suele significarQué hacer
tool.blocked_on_userduration_ms, decision, sourceEl 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 Bashduration_ms, success y, en el padre claude_code.tool, bash_argv0 y bash_command_classEl 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 cortosresponse.has_tool_call, stop_reasonBucle de exploración: lee archivo por archivo.Dale rutas concretas en el prompt o un mapa del repo.
llm_request con attempt mayor que 1status_code, error_classReintentos 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 altoinput_tokens, cache_read_tokens, cache_creation_tokensContexto 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 Agentsubagent_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:

  1. Exporter de consola. Cambia a OTEL_TRACES_EXPORTER=console. Si no ves spans en la salida, falta CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 o lanzaste claude desde otra terminal sin las variables.
  2. Protocolo y puerto. grpc va al 4317. Si usas http/protobuf, el endpoint es el 4318. Mezclarlos es el error más habitual.
  3. Log de depuración. La documentación recomienda arrancar con claude --debug-file /tmp/claude-otel.log y buscar errores de exportación con la marca [3P telemetry].
  4. Contenedor vivo. docker ps debe listar jaeger. 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.usage y claude_code.token.usage, o el propio /usage, ya te lo dan sin trazas.
  • Quieres auditar permisos denegados. classifier_verdict y source están en el evento tool_decision, que va por OTEL_LOGS_EXPORTER. Jaeger solo guarda trazas, así que necesitas un backend de logs.
  • Quieres medir tus hooks. El span claude_code.hook exige tracing detallado beta (ENABLE_BETA_TRACING_DETAILED=1 y BETA_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.

Compartir

X LinkedIn
Volver al blog