Distributed tracing pour agents : traçage des systèmes multi-agents

Le distributed tracing suit un run à travers plusieurs services, queues, tools et providers LLM tout en conservant le trace context end-to-end.
Sur cette page
  1. Idée en 30 secondes
  2. Problème principal
  3. Comment ça fonctionne
  4. À quoi ressemble un distributed trace
  5. Quand l'utiliser
  6. Exemple d'implémentation
  7. Erreurs typiques
  8. Nouveau trace_id dans chaque service
  9. Seul trace_id est propagé, sans relation de spans
  10. Le contexte est perdu dans les queues async
  11. Pas de service_name ni operation_name
  12. Auto-vérification
  13. FAQ
  14. Pages liées

Idée en 30 secondes

Le distributed tracing montre un run non pas dans un seul service, mais sur toute la chaîne d'appels.

Dans les systèmes multi-agents, une requête passe souvent par gateway, runtime, tools, queues et providers LLM.

Le distributed tracing relie ces étapes via trace_id, span_id et parent_span_id, donc le comportement du système devient visible end-to-end.

Problème principal

Quand un agent fonctionne sur plusieurs services, les logs sont généralement dispersés.

On voit séparément les erreurs du gateway, du tool service, et du runtime agent. Mais ces événements ne sont pas liés comme un seul run. Sans trace context partagé, il est difficile de comprendre qu'il s'agit du même run.

Résultat : même un incident simple devient une longue investigation :

  • on ne sait pas clairement dans quel service la latence est apparue ;
  • on ne voit pas où le contexte s'est perdu ;
  • il est difficile de relier les retries entre services ;
  • il est difficile de reconstruire le chemin complet d'un run problématique.

C'est pourquoi les systèmes multi-agents ont besoin de distributed tracing, et pas seulement de tracing local dans un seul runtime.

Comment ça fonctionne

Le distributed tracing utilise le même modèle trace et span, mais sur plusieurs services.

  • trace — le chemin complet d'une requête à travers tous les services
  • span — une opération concrète dans un service

Dans les systèmes réels, ces champs reposent généralement sur OpenTelemetry (OTel) :

  • trace_id — identifiant partagé du chemin complet
  • span_id — identifiant de l'étape courante
  • parent_span_id — lien avec l'étape parente
  • service_name — où l'étape a été exécutée
  • operation_name — ce que le service a fait

Pour éviter qu'un trace se casse, le trace context doit être propagé entre services à chaque appel. Le plus souvent via headers (traceparent) ou metadata de messages de queue.

À quoi ressemble un distributed trace

Le plus simple pour comprendre le distributed tracing est un exemple d'une requête.

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

Ce trace montre :

  • le chemin complet entre services ;
  • quel service a introduit la plus forte latence ;
  • où le contexte s'est rompu (si cela arrive) ;
  • quels spans sont parents et lesquels sont enfants.

Quand l'utiliser

Le distributed tracing n'est pas toujours nécessaire.

Si le système est monolithique et que tout le run vit dans un seul process, un tracing local suffit souvent.

Mais le distributed tracing devient critique quand :

  • une requête traverse plusieurs services ;
  • le workflow contient des queues ou des workers async ;
  • plusieurs agents échangent des événements ;
  • vous avez besoin d'une analyse précise de latency et retries entre services.

Exemple d'implémentation

Ci-dessous, un exemple simplifié de propagation du trace context entre gateway et service worker. Dans l'exemple, des headers simplifiés (x-trace-id, x-parent-span-id) sont utilisés pour montrer le principe de propagation. En production, on utilise en général le header W3C standard traceparent (via OpenTelemetry), qui propage automatiquement le trace context entre services.

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)  # appel HTTP/gRPC exemple vers worker_handle_request sur un autre service
        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:
        # ... travail agent, tool calls, étapes 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)

Même l'approche manuelle ci-dessus aide à comprendre la mécanique de base du distributed tracing.

Dans un workflow réel, chaque service crée en général son propre span puis propage ce span_id comme parent_span_id vers le hop suivant. Si cette étape est ignorée, le service suivant démarre un nouveau trace et la vue end-to-end casse.

Par exemple, un span-event en log JSON peut ressembler à ceci :

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"
}

Erreurs typiques

Même si le distributed tracing est déjà en place, des problèmes typiques restent fréquents en production.

Nouveau trace_id dans chaque service

Si chaque service génère son propre trace_id, le trace end-to-end se casse en morceaux. Dans ce mode, il devient difficile de localiser la cause d'un incident entre services.

Seul trace_id est propagé, sans relation de spans

trace_id sans span_id ni parent_span_id donne seulement une liste plate d'événements. Sans arbre de spans, difficile de comprendre quelles étapes étaient imbriquées.

Le contexte est perdu dans les queues async

Si la metadata de queue ne contient pas le trace context, une partie du workflow sort du trace. Ces coupures masquent souvent une phase précoce de panne partielle ou de pannes en cascade.

Pas de service_name ni operation_name

Sans ces champs, on voit qu'une erreur existe, mais on ne sait pas dans quel service et quelle opération. Le debugging prend alors beaucoup plus de temps.

Auto-vérification

Ci-dessous, une checklist courte de base pour le distributed tracing avant release.

Progression: 0/9

⚠ L'observability de base manque

Le système sera difficile à déboguer en production. Commencez par run_id, structured logs et tracing des tool calls.

FAQ

Q : Quelle différence entre distributed tracing et agent tracing classique ?
R : Agent tracing montre les étapes dans une seule runtime. Distributed tracing relie les étapes entre plusieurs services dans un trace end-to-end.

Q : Que se casse-t-il si on propage seulement trace_id entre services, sans span_id ni parent_span_id ?
R : Le trace devient plat : on voit que les événements appartiennent à un même chemin, mais il devient difficile de comprendre les étapes imbriquées, quel service a appelé lequel, et où exactement la latence ou l'erreur est apparue. trace_id relie le chemin global, tandis que span_id et parent_span_id construisent l'arbre des étapes.

Q : Comment propager le trace context via les queues ?
R : Ajoutez trace_id, span_id et parent_span_id dans la metadata des messages. Sinon les étapes async sortent du trace.

Q : Faut-il absolument adopter OpenTelemetry immédiatement ?
R : Non. Vous pouvez commencer avec propagation manuelle et logs structurés, puis migrer vers OTel SDK quand le système grandit.

Pages liées

Suite du sujet :

⏱️ 7 min de lectureMis à jour 21 mars 2026Difficulté: ★★★
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Permissions outils (allowlist / blocklist)
  • Kill switch & arrêt incident
  • Idempotence & déduplication
  • Audit logs & traçabilité
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.

Auteur

Nick — ingénieur qui construit une infrastructure pour des agents IA en production.

Focus : patterns d’agents, modes de défaillance, contrôle du runtime et fiabilité des systèmes.

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


Note éditoriale

Cette documentation est assistée par l’IA, avec une responsabilité éditoriale humaine pour l’exactitude, la clarté et la pertinence en production.

Le contenu s’appuie sur des défaillances réelles, des post-mortems et des incidents opérationnels dans des systèmes d’agents IA déployés.