Distributed tracing para agentes: trazado de sistemas multiagente

Distributed tracing permite seguir un run a través de varios servicios, colas, tools y proveedores LLM, manteniendo trace context end-to-end.
En esta página
  1. Idea en 30 segundos
  2. Problema principal
  3. Cómo funciona
  4. Cómo se ve un distributed trace
  5. Cuándo usar
  6. Ejemplo de implementación
  7. Errores típicos
  8. Nuevo trace_id en cada servicio
  9. Se propaga solo trace_id, sin relación de spans
  10. El contexto se pierde en colas async
  11. Falta service_name y operation_name
  12. Autoevaluación
  13. FAQ
  14. Páginas relacionadas

Idea en 30 segundos

Distributed tracing muestra un run no dentro de un solo servicio, sino en toda la cadena de llamadas.

En sistemas multiagente, una solicitud suele pasar por gateway, runtime, tools, colas y proveedores LLM.

Distributed tracing conecta esos pasos con trace_id, span_id y parent_span_id, por eso el comportamiento del sistema se ve end-to-end.

Problema principal

Cuando un agente opera en varios servicios, los logs suelen quedar dispersos.

Se ven por separado errores de gateway, de tool service y de agent runtime. Pero esos eventos no quedan ligados como un solo run. Sin trace context compartido, cuesta entender que es el mismo run.

Como resultado, incluso un incidente simple se convierte en investigación larga:

  • no está claro en qué servicio apareció la latencia;
  • no se entiende dónde se perdió el contexto;
  • cuesta conectar retries entre servicios;
  • es difícil reconstruir el camino completo de un run problemático.

Por eso los sistemas multiagente necesitan distributed tracing, no solo tracing local dentro de un runtime.

Cómo funciona

Distributed tracing usa el mismo modelo de trace y span, pero entre varios servicios.

  • trace — todo el camino de una solicitud por todos los servicios
  • span — una operación concreta en un servicio

En sistemas reales, estos campos suelen basarse en OpenTelemetry (OTel):

  • trace_id — identificador compartido del camino completo
  • span_id — identificador del paso actual
  • parent_span_id — vínculo con el paso padre
  • service_name — dónde se ejecutó el paso
  • operation_name — qué hizo el servicio

Para que el trace no se corte, el trace context debe propagarse entre servicios en cada llamada. Normalmente se hace por headers (traceparent) o metadata en mensajes de cola.

Cómo se ve un distributed trace

La forma más simple de entender distributed tracing es un ejemplo de una solicitud.

TEXT
trace_id: tr_7a31
user_query: "Find vendor invoices for March"

gateway         span g1   parent=-     18ms   status=ok
agent_runtime   span a1   parent=g1   240ms   status=ok
tool_service    span t1   parent=a1   410ms   status=ok
agent_runtime   span a2   parent=a1   130ms   status=ok
llm_provider    span l1   parent=a2   690ms   status=ok

stop_reason: completed

Ese trace muestra:

  • el camino completo entre servicios;
  • qué servicio aportó la mayor latencia;
  • dónde exactamente se rompió el contexto (si pasó);
  • qué spans fueron padres y cuáles hijos.

Cuándo usar

Distributed tracing no siempre es necesario.

Si el sistema es monolítico y todo el run vive en un solo proceso, muchas veces alcanza tracing local.

Pero distributed tracing se vuelve crítico cuando:

  • una solicitud pasa por varios servicios;
  • en el workflow hay colas o workers async;
  • varios agentes intercambian eventos;
  • se necesita análisis preciso de latency y retries entre servicios.

Ejemplo de implementación

Abajo hay un ejemplo simplificado de cómo propagar trace context entre gateway y un servicio worker. En el ejemplo se usan headers simplificados (x-trace-id, x-parent-span-id) para mostrar el principio de propagación. En producción normalmente se usa el header estándar W3C traceparent (vía OpenTelemetry), que propaga automáticamente trace context entre servicios.

PYTHON
import contextvars
import logging
import time
import uuid

logger = logging.getLogger("distributed-tracing")
trace_id_ctx = contextvars.ContextVar("trace_id", default=None)
span_id_ctx = contextvars.ContextVar("span_id", default=None)


def start_span(service_name, operation_name, parent_span_id=None):
    span_id = str(uuid.uuid4())
    started_at = time.time()
    logger.info(
        "span_started",
        extra={
            "trace_id": trace_id_ctx.get(),
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "service_name": service_name,
            "operation_name": operation_name,
        },
    )
    return span_id, started_at


def finish_span(service_name, operation_name, span_id, started_at, status, parent_span_id=None, error=None):
    logger.info(
        "span_finished",
        extra={
            "trace_id": trace_id_ctx.get(),
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "service_name": service_name,
            "operation_name": operation_name,
            "status": status,
            "latency_ms": int((time.time() - started_at) * 1000),
            "error": error,
        },
    )


def inject_context(headers):
    headers["x-trace-id"] = trace_id_ctx.get() or ""
    headers["x-parent-span-id"] = span_id_ctx.get() or ""


def extract_context(headers):
    incoming_trace_id = headers.get("x-trace-id") or str(uuid.uuid4())
    incoming_parent_span_id = headers.get("x-parent-span-id")
    trace_token = trace_id_ctx.set(incoming_trace_id)
    return incoming_parent_span_id, trace_token


def gateway_handle_request():
    trace_id = str(uuid.uuid4())
    trace_token = trace_id_ctx.set(trace_id)

    root_span_id, root_started_at = start_span("gateway", "handle_request", parent_span_id=None)
    span_token = span_id_ctx.set(root_span_id)

    try:
        headers = {}
        inject_context(headers)
        call_worker_service(headers)  # ejemplo de llamada HTTP/gRPC a worker_handle_request en otro servicio
        finish_span(
            "gateway",
            "handle_request",
            root_span_id,
            root_started_at,
            status="ok",
            parent_span_id=None,
        )
    except Exception as error:
        finish_span(
            "gateway",
            "handle_request",
            root_span_id,
            root_started_at,
            status="error",
            parent_span_id=None,
            error=str(error),
        )
        raise
    finally:
        span_id_ctx.reset(span_token)
        trace_id_ctx.reset(trace_token)


def worker_handle_request(headers):
    parent_span_id, trace_token = extract_context(headers)
    span_id, started_at = start_span("worker", "process_task", parent_span_id=parent_span_id)
    span_token = span_id_ctx.set(span_id)

    try:
        # ... trabajo del agente, tool calls, pasos LLM ...
        finish_span("worker", "process_task", span_id, started_at, status="ok", parent_span_id=parent_span_id)
    except Exception as error:
        finish_span(
            "worker",
            "process_task",
            span_id,
            started_at,
            status="error",
            parent_span_id=parent_span_id,
            error=str(error),
        )
        raise
    finally:
        span_id_ctx.reset(span_token)
        trace_id_ctx.reset(trace_token)

Incluso el enfoque manual de arriba ayuda a entender la mecánica base de distributed tracing.

En un workflow real, cada servicio suele crear su propio span y después pasar su span_id como parent_span_id al siguiente hop. Si se omite ese paso, el siguiente servicio iniciará un trace nuevo y se rompe la visión end-to-end.

Por ejemplo, un span-event en log JSON puede verse así:

JSON
{
  "timestamp": "2026-03-21T15:17:00Z",
  "event": "span_finished",
  "trace_id": "tr_7a31",
  "span_id": "sp_worker_02",
  "parent_span_id": "sp_gateway_01",
  "service_name": "worker",
  "operation_name": "process_task",
  "latency_ms": 410,
  "status": "ok"
}

Errores típicos

Incluso con distributed tracing ya implementado, en production siguen apareciendo problemas típicos.

Nuevo trace_id en cada servicio

Si cada servicio genera su propio trace_id, el trace end-to-end se rompe en fragmentos. En ese modo, cuesta localizar la causa raíz del incidente entre servicios.

Se propaga solo trace_id, sin relación de spans

trace_id sin span_id ni parent_span_id da solo una lista plana de eventos. Sin árbol de spans, cuesta entender qué pasos estaban anidados.

El contexto se pierde en colas async

Si la metadata de la cola no incluye trace context, parte del workflow queda fuera del trace. Esos cortes suelen ocultar una fase temprana de caída parcial o de fallos en cascada.

Falta service_name y operation_name

Sin esos campos se ve que hay un error, pero no queda claro en qué servicio y operación ocurrió. Eso vuelve el debugging mucho más lento.

Autoevaluación

Abajo tienes un checklist corto de distributed tracing base antes del release.

Progreso: 0/9

⚠ Falta observability base

Será difícil depurar el sistema en production. Empieza con run_id, structured logs y tracing de tool calls.

FAQ

Q: ¿En qué se diferencia distributed tracing del agent tracing normal?
A: Agent tracing muestra pasos dentro de un runtime. Distributed tracing conecta pasos entre varios servicios en un solo trace end-to-end.

Q: ¿Qué se rompe si entre servicios se propaga solo trace_id, sin span_id ni parent_span_id?
A: El trace queda plano: se ve que los eventos pertenecen al mismo camino, pero cuesta entender qué pasos estaban anidados, qué servicio llamó a cuál y dónde exactamente apareció la latencia o el error. trace_id une el camino global, mientras span_id y parent_span_id construyen el árbol de pasos.

Q: ¿Cómo propagar trace context por colas?
A: Agrega trace_id, span_id y parent_span_id en metadata del mensaje. Si no, los pasos async quedan fuera del trace.

Q: ¿Es obligatorio implementar OpenTelemetry desde el primer día?
A: No. Puedes comenzar con propagación manual y logs estructurados, y luego pasar a OTel SDK cuando el sistema crezca.

Páginas relacionadas

Sigue con estos temas:

⏱️ 7 min de lecturaActualizado 21 de marzo de 2026Dificultad: ★★★
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.

Autor

Nick — ingeniero que construye infraestructura para agentes de IA en producción.

Enfoque: patrones de agentes, modos de fallo, control del runtime y fiabilidad del sistema.

🔗 GitHub: https://github.com/mykolademyanov


Nota editorial

Esta documentación está asistida por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

El contenido se basa en fallos reales, post-mortems e incidentes operativos en sistemas de agentes de IA desplegados.