Qu'est-ce qu'une trace distribuée
Quand une requête traverse cinq services en 800ms, comment savoir lequel a volé 700ms ? Les métriques te disent qu'il y a un problème. Les traces te disent où exactement.
Imagine une commande sur un site e-commerce. Elle touche successivement l'API gateway, le service d'authentification, le service produits, une base de données, et un cache Redis. Chaque service loggue des événements. Chaque service expose des métriques. Mais si la commande prend 2 secondes au lieu de 200ms, aucun log ni aucune métrique ne te dit où exactement le temps a disparu.
C'est précisément ce que résout la trace distribuée.
Un enregistrement de bout en bout du chemin complet emprunté par une requête à travers plusieurs services, avec les durées de chaque étape. Une trace est composée de spans — des unités d'opération hiérarchisées dans un arbre.
Les trois piliers — rôles complémentaires
| Signal | Répond à | Granularité |
|---|---|---|
| Métriques | Est-ce que ça va ? Combien ? | Agrégé — pas de contexte individuel |
| Logs | Que s'est-il passé ? | Événement — pas de vision bout-en-bout |
| Traces | Où le temps a-t-il été dépensé ? | Requête individuelle — chemin complet |
La visualisation en waterfall
Une trace se lit comme un diagramme de Gantt : chaque ligne est un span, positionné horizontalement selon son heure de début, sa largeur représentant sa durée. Les spans enfants sont indentés sous leur parent. Tu vois instantanément les opérations séquentielles vs parallèles, et où le temps s'accumule.
Le simulateur ci-dessous anime une trace en temps réel : les spans apparaissent dans l'ordre où ils ont été créés, puis le waterfall complet se lit d'un coup.
Observe la trace se construire au fil de la requête — puis lis le waterfall pour identifier d'un coup d'œil où le temps a été dépensé.
Spans — l'unité de base
Une trace est un arbre de spans. Chaque span représente une opération : un appel HTTP, une requête SQL, un appel de service externe. Comprendre un span, c'est comprendre tout le modèle.
Un span est l'enregistrement d'une opération unitaire dans une trace. Il a un début, une fin, un nom, et peut transporter un grand nombre d'informations contextuelles. Plusieurs spans forment un arbre : le span racine représente la requête entière, les spans enfants représentent les opérations internes.
name · trace_id · span_id · parent_span_id · start_time · end_time · status · kind · attributes · events
Attributs vs Événements
Propriétés du span qui décrivent l'opération dans son ensemble. Définis au début et à la fin du span.
"http.method": "POST" "http.url": "/api/orders" "http.status_code": 200 "db.system": "postgresql" "db.statement": "SELECT * FROM orders"
Points dans le temps à l'intérieur du span. Utiles pour enregistrer des erreurs ou des étapes intermédiaires.
{ "name": "exception", "timestamp": "14:32:09.412", "attributes": { "exception.type": "TimeoutException", "exception.message": "…" } }
Status et Kind
| Champ | Valeurs | Usage |
|---|---|---|
status | UNSET · OK · ERROR | Résultat de l'opération. ERROR déclenche l'alerte Tempo. |
kind | SERVER · CLIENT · PRODUCER · CONSUMER · INTERNAL | Rôle du span dans l'architecture (entrant vs sortant). |
http.method, db.system, rpc.service sont des noms officiels. Les utiliser permet à Tempo et Grafana de les interpréter automatiquement.
Construis un span dans le simulateur ci-dessous en ajoutant des attributs et des événements. Observe comment le JSON évolue.
Ajoute des informations au span :
Propagation de contexte W3C TraceContext
Sans propagation, chaque service voit sa propre trace isolée. L'en-tête traceparent est le fil invisible qui relie tous les spans d'une même requête en un seul arbre.
Une trace distribuée n'a de sens que si tous les services qui traitent une requête sont conscients du même identifiant de trace. Sans mécanisme de propagation, chaque service crée sa propre trace isolée et le lien entre elles est perdu.
Le mécanisme par lequel un service injecte l'identifiant de trace courant dans les appels sortants (en-têtes HTTP, métadonnées gRPC, champs de message queue), et par lequel le service destinataire extrait ces informations pour créer ses propres spans comme enfants de la trace parente.
Le standard W3C TraceContext
Le W3C a standardisé le format des en-têtes de propagation en 2021. Deux en-têtes :
# Format : version-traceId(128bit)-parentSpanId(64bit)-flags traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 # Données vendor-specific optionnelles tracestate: grafana=abc123,dd=xyz789
| Champ | Longueur | Exemple |
|---|---|---|
| version | 2 hex | 00 |
| trace-id | 32 hex (128 bits) | 0af7651916cd43dd8448eb211c80319c |
| parent-span-id | 16 hex (64 bits) | b7ad6b7169203331 |
| trace-flags | 2 hex | 01 = sampled, 00 = non sampled |
Avant W3C TraceContext, Zipkin avait défini le format B3 avec les en-têtes
X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId. Les SDK OpenTelemetry modernes supportent les deux formats. Préfère W3C TraceContext pour les nouveaux projets.
traceparent dans chaque requête HTTP sortante et l'extrait de chaque requête entrante. C'est transparent pour ton code métier.
Le simulateur ci-dessous montre étape par étape comment l'en-tête traceparent voyage entre trois services. Clique sur "Étape suivante" pour avancer.
OpenTelemetry — instrumenter son code
OpenTelemetry est le standard CNCF pour collecter métriques, logs et traces depuis n'importe quel langage, vers n'importe quel backend. Deux modes : auto-instrumentation et instrumentation manuelle.
OpenTelemetry (OTel) est le projet CNCF qui standardise la façon de collecter métriques, logs et traces depuis n'importe quel langage, vers n'importe quel backend. C'est le successeur d'OpenTracing et OpenCensus — les deux ont fusionné pour donner OTel.
API — l'interface stable que ton code appelle pour créer des spans. Peut être un no-op si aucun SDK n'est installé.
SDK — l'implémentation : collecte les spans, les échantillonne, les exporte.
Collector — un agent/proxy facultatif qui reçoit, transforme et relaie la télémétrie vers un ou plusieurs backends.
Auto-instrumentation vs instrumentation manuelle
Le SDK instrumente automatiquement les bibliothèques connues (HTTP, Doctrine, Guzzle, Redis…) via des hooks. Tu ne touches pas ton code.
# composer.json "open-telemetry/opentelemetry-auto-symfony": "^1.0" "open-telemetry/opentelemetry-auto-doctrine": "^1.0" "open-telemetry/opentelemetry-auto-psr18": "^1.0" # .env OTEL_PHP_AUTOLOAD_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 OTEL_SERVICE_NAME=my-api
Résultat : spans HTTP server, DB queries, appels HTTP clients — sans écrire une ligne d'OTel.
Pour les opérations métier importantes que l'auto-instrumentation ne voit pas.
use OpenTelemetry\API\Globals; $tracer = Globals::tracerProvider() ->getTracer('my-app'); $span = $tracer->spanBuilder('ProcessOrder') ->setSpanKind(SpanKind::KIND_INTERNAL) ->startSpan(); $span->setAttribute('order.id', $orderId); $span->setAttribute('order.total', $total); try { $this->processOrder($order); $span->setStatus(StatusCode::STATUS_OK); } catch (Exception $e) { $span->recordException($e); $span->setStatus(StatusCode::STATUS_ERROR); } finally { $span->end(); }
OTLP — le protocole de transport
OTLP (OpenTelemetry Protocol) est le format de sérialisation standard d'OTel pour envoyer les traces, métriques et logs. Deux transports : gRPC (port 4317) et HTTP/Protobuf (port 4318). Tempo, Grafana Mimir, et la plupart des backends modernes acceptent OTLP nativement.
SDK OTel
optionnel
port 4317/4318
Le simulateur ci-dessous visualise quels spans apparaissent automatiquement et comment un span manuel s'imbrique dans la trace. Clique sur "Ajouter span manuel" pour voir la différence.
Une requête POST /checkout — spans auto-instrumentés en gris · span manuel en violet.
Grafana Tempo — stocker et visualiser
Tempo stocke les traces dans un objet-store (S3-compatible) sans index full-text. La visualisation en waterfall te montre d'un coup d'œil où le temps a été dépensé.
Grafana Tempo est le backend de stockage des traces dans la stack LGTM. Sa particularité : il stocke les traces dans un objet-store (S3, GCS, Azure Blob) et n'indexe que l'identifiant de trace. Pas d'index full-text sur les attributs de spans — ce qui le rend extrêmement économique en comparaison de Jaeger ou Elasticsearch.
- Stockage objet (S3-compatible)
- Index minimal (trace_id uniquement)
- Recherche par attributs via TraceQL + index de blocs
- Pas de base de données externe
- Coût très faible à grande échelle
- Stockage Elasticsearch ou Cassandra
- Index full sur tous les tags/attributs
- Recherche rapide sur n'importe quel attribut
- Coût élevé à grande échelle
La vue Waterfall dans Grafana
Dans Grafana Explore, une trace s'affiche sous forme de waterfall : chaque span est une ligne horizontale, indentée selon sa profondeur dans l'arbre. La largeur représente la durée. La couleur indique le statut (vert = OK, rouge = ERROR).
exemplar: true dans les règles d'alerte et le SDK OTel PHP.
Intégration avec Loki
Si tes logs contiennent un champ trace_id, Grafana Loki peut configurer des Derived Fields qui créent automatiquement un lien cliquable vers Tempo. Tu passes en un clic d'un log d'erreur à la trace complète.
Dans le simulateur ci-dessous, clique sur un span pour voir ses attributs. Identifie le span ERROR et comprends pourquoi la requête a été lente.
Trace a3f8b1c2d4e5f607… · durée totale : 1 240ms
Sélectionne un span
TraceQL — requêter les traces
TraceQL est à Tempo ce que LogQL est à Loki : un langage pour filtrer, agréger et analyser les traces. Il opère sur les spans individuels et sur la structure des traces.
TraceQL est le langage de requête de Grafana Tempo. Contrairement à LogQL qui filtre des lignes, TraceQL filtre des spans — et peut raisonner sur la structure de la trace (relations parent-enfant, ordres entre spans).
La brique de base :
{conditions}. Retourne tous les spans qui satisfont les conditions. Depuis un span, on peut remonter à sa trace complète.
# Tous les spans en erreur {status = error} # Spans HTTP avec status 5xx {span.http.status_code >= 500} # Requêtes SQL lentes {span.db.system = "postgresql" && duration > 500ms} # Traces où payment-service a une erreur {resource.service.name = "payment-service" && status = error}
Opérateurs de structure
TraceQL peut exprimer des relations entre spans — ce qu'aucun autre système de logs ne peut faire.
| Opérateur | Signification | Exemple |
|---|---|---|
>> | descendant (n'importe quel niveau) | {.service="api"} >> {status=error} |
> | enfant direct | {.service="api"} > {.service="db"} |
& | les deux spans dans la même trace | {status=error} & {duration > 1s} |
! | not — exclure | !{span.http.url =~ ".*/health"} |
Metric queries
Comme LogQL, TraceQL a un mode metric query qui agrège des traces en séries temporelles :
# Taux d'erreurs par service { status = error } | by(resource.service.name) | rate() # P95 de la durée des spans HTTP { span.http.method != "" } | quantile_over_time(duration, 0.95) by(resource.service.name) # Nombre de spans par service les 5 dernières minutes { } | count() by(resource.service.name)
Le simulateur ci-dessous montre des traces de production simulées. Active des filtres TraceQL pour voir lesquelles correspondent.
Active des conditions TraceQL et observe quelles traces passent le filtre.
Synthèse — traces en production
Instrumenter, propager, collecter, stocker, requêter — le pipeline complet. Et les décisions qui font la différence : sampling, granularité, corrélation.
Les traces distribuées ferment la boucle de l'observabilité. Voici le pipeline complet, de l'instrumentation jusqu'à l'investigation.
SDK OTel / auto
traceparent W3C
OTLP → Collector
Tempo / S3
TraceQL
Sampling — ne pas tout enregistrer
En production, enregistrer 100 % des traces est souvent trop coûteux. Il faut échantillonner.
| Stratégie | Décision prise | Avantage | Inconvénient |
|---|---|---|---|
| Head sampling | À l'entrée de la requête (SDK) | Simple, faible overhead | Ne peut pas favoriser les erreurs (pas encore visibles) |
| Tail sampling | Après la trace complète (Collector) | Garde 100 % des erreurs et des lentes | Nécessite de buffériser les traces en mémoire |
tailsampling.
Pièges classiques
traceparent entrant, il génère un nouveau trace_id et la trace se coupe. Vérifie que tous tes clients HTTP (Guzzle, Symfony HttpClient) ont bien le propagateur W3C activé.
Les trois piliers — guide de choix définitif
| Scénario d'investigation | Commence par |
|---|---|
| Alerte sur un SLO (P99 dégradé) | Métriques → graphe Prometheus → exemplar → trace Tempo |
| Erreur remontée par un utilisateur ("ma commande a échoué") | Logs Loki → trace_id → trace Tempo |
| Requête lente signalée sur un endpoint spécifique | TraceQL → traces lentes → span le plus lent → attributs |
| Incident généralisé (tous les services dégradés) | Métriques → dashboard → timeline des changements → logs |