Mascota Jacar — leyendo contigo Un portátil cuyos ojos siguen el cursor mientras lees.
Inteligencia Artificial Tecnología

Observabilidad de agentes con OpenTelemetry GenAI semconv en 2026

Observabilidad de agentes con OpenTelemetry GenAI semconv en 2026

La especificación OpenTelemetry GenAI semconv define ya en 2026 los atributos estándar para instrumentar llamadas a LLMs, ejecuciones de herramientas y operaciones de agentes; este post enseña cómo aplicarla a un agente real en Python con el SDK de Anthropic, recolectarla con un OTel Collector y consultarla en Grafana Tempo. Ver también: la guía completa del mcp model context protocol que enmarca la pieza MCP de la instrumentación.

Puntos clave

  • La spec OTel GenAI ha pasado por su ventana de estabilización en 2025 y, en 2026, los nombres de span (chat, execute_tool, invoke_agent) y los atributos clave son consistentes entre proveedores.
  • Hay dos atributos para identificar al proveedor: el moderno gen_ai.provider.name y el histórico gen_ai.system. En 2026 conviene emitir ambos: muchos colectores y dashboards aún leen el segundo.
  • Para tool use clásico hay gen_ai.tool.name y gen_ai.tool.call.id dentro de un span execute_tool. Para servidores MCP hay una sub-spec con mcp.method.name, mcp.protocol.version y mcp.session.id.
  • La capa de recolección estándar es OTel Collector con receiver otlp, processors memory_limiter + batch y exporter otlphttp apuntando a Tempo. Sin memory_limiter el collector se cae el día menos pensado.
  • TraceQL acepta filtros sobre atributos GenAI: { name = "chat" && span.gen_ai.usage.input_tokens > 1000 } es una consulta válida sobre Tempo.
  • Anthropic, LangSmith y Braintrust ya emiten atributos GenAI semconv, así que un panel construido contra estos atributos sirve aunque cambies de proveedor o de framework de agentes.

Qué cubre la semconv GenAI en 2026

Las convenciones semánticas GenAI de OpenTelemetry[1] cubren cinco familias de señales: events de entrada y salida, exceptions, metrics, spans de operación de modelo y spans de operación de agente. Sobre esa base, la spec añade conventions específicas por proveedor (Anthropic, OpenAI, Azure AI Inference, AWS Bedrock) y, desde finales de 2025, una sub-spec dedicada a servidores MCP. El estado formal del grupo gen-ai es “Development” en 2026 con varias áreas en release-candidate, pero en la práctica los nombres de span y atributos clave llevan estables más de un año y los SDKs comerciales los emiten con el mismo shape.

Lo importante operativamente es que esto convierte el dashboard del agente en algo portable: un panel de p99 de chat o un alert de tasa de errores en execute_tool no se reescribe cuando cambias de Claude a GPT, ni cuando migras de instrumentación manual a una librería tipo OpenLLMetry. La inversión que en pilas anteriores había que hacer dos veces —una para instrumentar function-calling propietario y otra para ETLificar logs— se hace una sola vez.

Conviene separar dos clases de spans. Los model-operation spans son cada llamada concreta al LLM: chat, text_completion, embeddings. Los agent-operation spans son las operaciones del agente alrededor: invoke_agent, create_agent, execute_tool. La consecuencia es que la traza de un turno típico tiene jerarquía: un span invoke_agent contiene uno o varios chat, y cada chat que pida una herramienta cuelga de sí un execute_tool. La pillar mcp model context protocol explica el contexto del que sale ese árbol.

Atributos clave: gen_ai.system, request, response, usage

El bloque mínimo de atributos a emitir en un span chat se reparte en cuatro categorías ortogonales. Identificación del proveedor: gen_ai.provider.name con valor canónico (anthropic, openai, azure.ai.openai, aws.bedrock) y, por compatibilidad con instrumentaciones más antiguas, gen_ai.system con un valor humano (Anthropic). Operación: gen_ai.operation.name ∈ {chat, text_completion, embeddings, execute_tool, invoke_agent, create_agent}. Request: gen_ai.request.model, y opcionalmente gen_ai.request.max_tokens, temperature, top_p, stop_sequences. Response: gen_ai.response.model (puede diferir si el proveedor enruta a una variante), gen_ai.response.id, gen_ai.response.finish_reasons. Usage: gen_ai.usage.input_tokens y gen_ai.usage.output_tokens — la pareja sobre la que se construyen casi todos los dashboards de coste.

python
# app/observability.py — wrapper chat() con atributos GenAI semconv
from contextlib import contextmanager
from opentelemetry import trace
import anthropic

tracer = trace.get_tracer("agente-anthropic-sdk")
client = anthropic.Anthropic()


@contextmanager
def chat(model: str, messages: list[dict]):
    with tracer.start_as_current_span(f"chat {model}", kind=trace.SpanKind.CLIENT) as span:
        span.set_attribute("gen_ai.provider.name", "anthropic")
        span.set_attribute("gen_ai.system", "Anthropic")  # back-compat
        span.set_attribute("gen_ai.operation.name", "chat")
        span.set_attribute("gen_ai.request.model", model)
        try:
            response = client.messages.create(model=model, max_tokens=1024, messages=messages)
            span.set_attribute("gen_ai.response.model", response.model)
            span.set_attribute("gen_ai.response.id", response.id)
            span.set_attribute("gen_ai.usage.input_tokens", response.usage.input_tokens)
            span.set_attribute("gen_ai.usage.output_tokens", response.usage.output_tokens)
            yield response
        except Exception as exc:
            span.record_exception(exc)
            span.set_status(trace.Status(trace.StatusCode.ERROR))
            raise

Dos detalles que pagan la pena. El nombre del span sigue el patrón {operation.name} {model} cuando el modelo se conoce de antemano (chat claude-opus-4-7), o solo {operation.name} cuando no. La cardinalidad se mantiene baja porque el modelo cambia muy rara vez. Y la excepción se registra con span.record_exception() y span.set_status(StatusCode.ERROR) antes de relanzar — sin eso, los filtros TraceQL por status = error no encuentran nada.

Si usas Anthropic con prompt caching, conviene poblar también dos atributos vendor-specific bajo el mismo prefijo: gen_ai.usage.input_tokens.cache_read y gen_ai.usage.input_tokens.cache_write. Son extensiones — no parte estricta de la spec — pero los SDKs maduros ya las exponen y son las que permiten verificar en producción que el caching efectivo está pegando.

Spans para tool use y MCP servers

El tool use clásico —function-calling sin MCP— se instrumenta con un span hijo execute_tool. Los atributos requeridos son gen_ai.tool.name (el nombre que ve el modelo) y gen_ai.tool.call.id (el identificador devuelto por la API: toolu_... en Anthropic, call_... en OpenAI). Eso permite cruzar logs y trazas en Tempo cuando una herramienta se queja.

python
# app/tool_span.py — span hijo execute_tool
from opentelemetry import trace
tracer = trace.get_tracer("agente-anthropic-sdk")


def run_tool(tool_use_block, fn) -> str:
    with tracer.start_as_current_span(f"execute_tool {tool_use_block.name}") as span:
        span.set_attribute("gen_ai.operation.name", "execute_tool")
        span.set_attribute("gen_ai.tool.name", tool_use_block.name)
        span.set_attribute("gen_ai.tool.call.id", tool_use_block.id)
        span.set_attribute("gen_ai.tool.type", "function")
        return fn(tool_use_block.input)

Cuando la herramienta es un servidor MCP, la cosa cambia: la sub-página de la spec opentelemetry.io semconv GenAI MCP[2] define un set propio que extiende al genérico. Los obligatorios son mcp.method.name (con valores tools/call, tools/list, initialize…), y los recomendados son mcp.protocol.version (p.ej. 2025-06-18), mcp.session.id y, cuando aplica, mcp.resource.uri. El nombre de span del lado servidor sigue el patrón {mcp.method.name} {target}tools/call create_ticket, por ejemplo — con span kind = SERVER y relación parent-child con el cliente.

python
# mcp_server/observability.py — span MCP del lado servidor
from opentelemetry import trace
tracer = trace.get_tracer("mcp-dominio-server")

def traced_tools_call(tool_name: str, session_id: str, fn, *args, **kwargs):
    with tracer.start_as_current_span(
        f"tools/call {tool_name}", kind=trace.SpanKind.SERVER
    ) as span:
        span.set_attribute("mcp.method.name", "tools/call")
        span.set_attribute("mcp.protocol.version", "2025-06-18")
        span.set_attribute("mcp.session.id", session_id)
        span.set_attribute("gen_ai.tool.name", tool_name)
        return fn(*args, **kwargs)

La consecuencia operativa es que la traza de un turno con MCP tiene cuatro niveles: chat → execute_tool → tools/call → POST https://.... Cada nivel está en una pila distinta —cliente Anthropic, agente, servidor MCP, backend HTTP— pero el traceparent se propaga por el tránsito JSON-RPC y por el cliente HTTP del servidor, así que el árbol queda completo. La sibling cluster de tutorial del SDK de Anthropic muestra el código del agente que produce esa jerarquía.

Instrumentación en Python con Anthropic SDK

En 2026, los SDKs comerciales emiten atributos GenAI semconv de tres maneras. La documentación oficial del SDK de Anthropic[3] describe el shape de response.usage —del que sale la pareja input_tokens/output_tokens— pero no instrumenta automáticamente; el patrón canónico es el wrapper manual del bloque anterior, o el paquete opentelemetry-instrumentation-anthropic de la familia OpenLLMetry para auto-instrumentación. LangSmith exporta spans a OTLP cuando defines LANGSMITH_TRACING=true y un endpoint OTLP, con gen_ai.system y la familia gen_ai.usage.* ya pobladas. Braintrust, desde mediados de 2025, emite spans GenAI semconv-conformes vía su SDK y expone una integración OTLP directa.

La consecuencia práctica es que el dashboard funciona aunque la pila debajo cambie. Un panel de p99 de chat, una alerta sobre gen_ai.usage.input_tokens > 8000 o un contador de errores en execute_tool se construyen una vez y siguen funcionando si mañana el equipo de plataforma sustituye Anthropic por Bedrock, o si el equipo de evals añade Braintrust al stack. Tienes la implementación completa del wrapper OTel y la configuración del Collector en el repositorio de referencia github.com/jacarsystems/jacar-anthropic-sdk-demo[4] — concretamente bajo app/observability.py y ops/otel-collector.yaml.

Si vas a empezar de cero, elige una de las dos vías y no las mezcles. La instrumentación manual da control total sobre el shape del span y es ideal cuando el agente es una pieza pequeña en un servicio mayor que ya tiene su propia política de tracing. La auto-instrumentación con OpenLLMetry o equivalente es la vía rápida cuando el agente es el servicio principal y prefieres invertir el tiempo en dashboards en lugar de en boilerplate. Combinar las dos suele acabar en spans duplicados que ensucian el árbol, y es un dolor concreto de depurar.

Recolección con OTel Collector → Tempo

El otro lado de la instrumentación es la recolección. El OTel Collector es el agente que recibe spans de la aplicación, los procesa y los exporta. La forma canónica en 2026 son tres bloques: receivers OTLP (gRPC en :4317, HTTP en :4318), processors memory_limiter y batch, y un exporter otlphttp apuntando a Tempo. Tres detalles importan más de lo que parece. memory_limiter no es opcional: sin él, un pico de tráfico tira el collector con OOM antes de que el orquestador pueda reaccionar. El batch debe agrupar al menos cinco segundos para que el coste de red sea razonable. Y el exporter debug (antes llamado logging) ayuda a depurar la integración local sin tocar la lógica de la aplicación.

yaml
# ops/otel-collector.yaml — drop-in para /etc/otelcol-contrib/config.yaml
receivers:
  otlp:
    protocols:
      grpc: { endpoint: 0.0.0.0:4317 }
      http: { endpoint: 0.0.0.0:4318 }

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  batch:
    send_batch_size: 8192
    timeout: 5s

exporters:
  otlphttp/tempo:
    endpoint: http://tempo:4318
    tls: { insecure: true }
  debug: { verbosity: basic }

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/tempo, debug]

Para un despliegue real, dos extensiones merecen atención. El connector spanmetrics deriva métricas RED (rate, errors, duration) de los spans entrantes — eso significa que tienes p99 de chat o execute_tool en Prometheus sin tocar el código de aplicación. Y el processor tail_sampling permite descartar trazas triviales cuando el volumen pega: por ejemplo, conservar solo trazas con span.gen_ai.usage.input_tokens > 0 o con errores. Las decisiones por defecto sobre qué stack abierto usar siguen las del post sobre observabilidad 2026: Prometheus para métricas, Loki para logs, Tempo para trazas, Grafana para todo lo visible.

Una nota sobre logs: la instrumentación de logs con OTel se cubre aparte —ver OpenTelemetry: la unificación de logs, métricas y trazas— y conviene levantarla en paralelo si quieres correlacionar los console.log de la aplicación con los spans del agente vía trace_id y span_id. Esto es lo que en Tempo permite el “Logs for this span” en un click.

Cuadro de mando: latencia, tokens, errores por agente

Con los spans bien atribuidos y el collector enrutando a Tempo, las consultas TraceQL salen casi solas. La sintaxis es la documentada en grafana.com/oss/tempo[5] y la referencia oficial de TraceQL[6], con curly-braces, atributos prefijados con span. o resource., y combinadores && / ||. Tres consultas cubren el 80% del trabajo operativo:

bash
# Tempo TraceQL — chats lentos por encima de 5s
{ name = "chat claude-opus-4-7" && duration > 5s }

# tool use con error en el último día
{ name =~ "execute_tool .*" && status = error }

# turnos con consumo alto de tokens (gasto inesperado)
{ name = "chat claude-opus-4-7" && span.gen_ai.usage.input_tokens > 8000 }

Para que estos filtros se conviertan en paneles vivos, el camino habitual es pasar de TraceQL a métricas vía el spanmetrics connector y, sobre Mimir o Prometheus, definir una recording rule estable. Esto desacopla el dashboard del coste de scan que tiene una consulta TraceQL grande:

yaml
# Mimir/Prometheus recording rule — p99 de chat por modelo
groups:
  - name: jacar-genai
    interval: 30s
    rules:
      - record: gen_ai_chat_p99_latency_seconds
        expr: histogram_quantile(0.99,
          sum by (le, gen_ai_request_model) (
            rate(traces_span_metrics_duration_seconds_bucket{span_name="chat"}[5m])
          ))

Sobre esa base, el cuadro de mando que recomendaría en 2026 tiene cuatro paneles. Latencia (gen_ai_chat_p99_latency_seconds por modelo y por agente). Tokens (sum by (gen_ai_request_model) (rate(gen_ai_usage_input_tokens[5m])) para input y output, dos series por modelo). Errores (tasa de status = error en spans chat y execute_tool, separadas porque la causa raíz es muy distinta). MCP (tasa de tools/call por servidor, latencia p99, errores). Si el equipo va a hacer evals con Braintrust o LangSmith en paralelo, tener estas cuatro vistas en abierto sobre los mismos atributos GenAI semconv evita que cada herramienta arme su propia narrativa. El patrón de despliegue del cuadro de mando se cruza con las recetas de sistemas multi-agente: LangGraph, CrewAI y AutoGen, porque cuando hay varios agentes coordinados, la dimensión agent_name aparece naturalmente en el atributo gen_ai.agent.name y se convierte en la dimensión principal del dashboard.

Conclusión

La observabilidad de agentes en 2026 es un problema resuelto siempre que partas de la spec OTel GenAI: instrumentas una vez con gen_ai.provider.name, gen_ai.operation.name, gen_ai.request.model y la pareja usage.input_tokens / usage.output_tokens, recolectas con OTel Collector hacia Tempo, y consultas con TraceQL. Las cuatro piezas —SDK del proveedor, wrapper de span, Collector, Tempo— son ortogonales y cada una se reemplaza por separado sin tocar el resto. La consecuencia es que el dashboard sobrevive a los cambios de modelo, de proveedor y de framework de agentes, que es lo que hace que la inversión pague la pena.

¿Te ha resultado útil?
[Total: 0 · Media: 0]
  1. convenciones semánticas GenAI de OpenTelemetry
  2. opentelemetry.io semconv GenAI MCP
  3. documentación oficial del SDK de Anthropic
  4. github.com/jacarsystems/jacar-anthropic-sdk-demo
  5. grafana.com/oss/tempo
  6. referencia oficial de TraceQL

Escrito por

CEO - Jacar Systems

Apasionado de la tecnología, la infraestructura cloud y la inteligencia artificial. Escribe sobre DevOps, IA, plataformas y software desde Madrid.