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 servicesspan— 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 completspan_id— identifiant de l'étape couranteparent_span_id— lien avec l'étape parenteservice_name— où l'étape a été exécutéeoperation_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.
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.
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 :
{
"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 :
- Observability pour agents IA — vue de base des traces, logs et metrics.
- Tracing d'agent — tracing d'un run dans un service.
- Debugging des runs d'agent — analyser les runs problématiques étape par étape.
- Failure alerting — détecter tôt les coupures et la dégradation.
- Métriques d'agents — quelles métriques sont nécessaires pour le monitoring de production.