← Tous les cours / Traces distribuées & OpenTelemetry Cours
00 — INTRODUCTION

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.

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

SignalRépond àGranularité
MétriquesEst-ce que ça va ? Combien ?Agrégé — pas de contexte individuel
LogsQue s'est-il passé ?Événement — pas de vision bout-en-bout
TracesOù le temps a-t-il été dépensé ?Requête individuelle — chemin complet
💡 Flux de diagnostic typique : une alerte Prometheus te dit que le P95 des requêtes dépasse 1s (métriques) → tu regardes les logs Loki pour identifier l'endpoint concerné (logs) → tu ouvres une trace Tempo pour voir quelle opération consomme 900ms sur ce chemin (traces). Les trois piliers se complètent, ils ne se remplacent pas.

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é.

01 — ANATOMIE D'UN SPAN

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.

Anatomie d'un span
name · trace_id · span_id · parent_span_id · start_time · end_time · status · kind · attributes · events

Attributs vs Événements

Attributes

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

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

ChampValeursUsage
statusUNSET · OK · ERRORRésultat de l'opération. ERROR déclenche l'alerte Tempo.
kindSERVER · CLIENT · PRODUCER · CONSUMER · INTERNALRôle du span dans l'architecture (entrant vs sortant).
💡 Conventions de nommage des attributs : OpenTelemetry définit un Semantic Conventions — un vocabulaire standard pour les attributs courants. 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 :

02 — PROPAGATION W3C

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.

Propagation de contexte
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
ChampLongueurExemple
version2 hex00
trace-id32 hex (128 bits)0af7651916cd43dd8448eb211c80319c
parent-span-id16 hex (64 bits)b7ad6b7169203331
trace-flags2 hex01 = sampled, 00 = non sampled
B3 — le format historique
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.
🔑 Les SDK font tout automatiquement. En pratique, tu n'écris jamais le code d'injection/extraction manuellement. Le SDK OpenTelemetry (ou l'auto-instrumentation) injecte 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.

03 — SDK OPENTELEMETRY

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.

Les trois composants 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

Auto-instrumentation

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.

Instrumentation manuelle

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.

App PHP
SDK OTel
OTLP →
OTel Collector
optionnel
OTLP →
Tempo
port 4317/4318
💡 Collector ou direct ? En développement, envoie directement vers Tempo. En production, passe par le Collector pour batcher les spans, appliquer du sampling, et router vers plusieurs backends sans changer le code applicatif.

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.

04 — TEMPO & WATERFALL

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.

Tempo
  • 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
Jaeger / Zipkin
  • 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).

💡 Exemplars — le lien métriques → traces. Prometheus peut stocker des exemplars sur ses métriques : un point de données avec le trace_id de la requête qui l'a généré. Dans Grafana, un clic sur un point de graphe Prometheus ouvre directement la trace Tempo correspondante. Pour l'activer : 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

← Clique sur un span

Sélectionne un span

05 — TRACEQL

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).

Sélecteur de span
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érateurSignificationExemple
>>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.

06 — SYNTHÈSE

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.

1. Instrumenter
SDK OTel / auto
2. Propager
traceparent W3C
3. Collecter
OTLP → Collector
4. Stocker
Tempo / S3
5. Requêter
TraceQL

Sampling — ne pas tout enregistrer

En production, enregistrer 100 % des traces est souvent trop coûteux. Il faut échantillonner.

StratégieDécision priseAvantageInconvénient
Head samplingÀ l'entrée de la requête (SDK)Simple, faible overheadNe peut pas favoriser les erreurs (pas encore visibles)
Tail samplingAprès la trace complète (Collector)Garde 100 % des erreurs et des lentesNécessite de buffériser les traces en mémoire
💡 En pratique : head sampling à 10–20 % pour le trafic nominal, + tail sampling pour conserver 100 % des traces en erreur. Configurable dans l'OTel Collector via le processeur tailsampling.

Pièges classiques

⚠️ Oublier de propager le contexte — si un service ne lit pas le header 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é.
💡 Trop de spans = bruit — instrumenter chaque ligne de code crée des milliers de spans par requête, difficiles à lire et coûteux à stocker. Réserve l'instrumentation manuelle aux opérations métier importantes (ProcessOrder, SendNotification) ou aux appels externes.
💡 Données sensibles dans les attributs — les attributs de spans arrivent dans Tempo et sont visibles à quiconque a accès au backend. Ne mets jamais de mot de passe, token, ni données personnelles dans les attributs de spans.

Les trois piliers — guide de choix définitif

Scénario d'investigationCommence 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écifiqueTraceQL → traces lentes → span le plus lent → attributs
Incident généralisé (tous les services dégradés)Métriques → dashboard → timeline des changements → logs
🔑 La stack LGTM complète : tu as maintenant les quatre briques — Loki (logs) · Grafana (visualisation) · Tempo (traces) · Mimir ou Prometheus (métriques). Les trois signaux parlent le même langage de labels et sont reliés par le trace_id. Une alerte, un log, une trace — tout s'ouvre depuis Grafana Explore sans changer d'outil.