infra / monitoring-observabilite
Tracing distribué : OpenTelemetry et Jaeger
Explication
Ce que vous allez apprendre
- Comprendre l'anatomie d'une trace : trace ID, spans, relations parent-enfant
- Instrumenter une application FastAPI avec le SDK OpenTelemetry
- Visualiser des traces dans Jaeger et interpréter la durée de chaque span
- Comprendre le rôle central du Collector OpenTelemetry dans une architecture d'observabilité
- Expliquer comment un trace ID voyage entre plusieurs services via la propagation de contexte
Dans quel contexte ?
Un client signale qu'une commande met parfois 3 secondes à se valider, parfois 300 millisecondes, sans schéma apparent. Les logs (leçon précédente) montrent bien chaque étape séparément dans chaque service, mais reconstituer manuellement le chemin exact d'UNE requête à travers l'API Gateway, le service d'authentification, le service de commande et l'API de paiement devient un puzzle. C'est exactement le problème que le tracing distribué résout.
D'abord, il faut comprendre ce qu'est une trace, concrètement
Une trace représente le parcours complet d'une seule requête, identifiée par un trace_id unique partagé par TOUS les services qu'elle traverse. À l'intérieur de cette trace, chaque étape individuelle (un appel HTTP, une requête base de données, un calcul) devient un "span", avec son propre nom, son heure de début et de fin, et éventuellement des attributs personnalisés.
Les spans s'organisent en arbre : un span racine (souvent l'entrée dans l'API Gateway) a des spans enfants, qui peuvent eux-mêmes avoir leurs propres enfants — reproduisant exactement la hiérarchie des appels réels entre services.
| Élément | Rôle | Exemple |
|---|---|---|
| Trace ID | Identifie toute la requête de bout en bout | abc123 |
| Span | Une étape unique avec début/fin | "DB Query" [30ms → 150ms] |
| Attribut | Métadonnée custom attachée à un span | order.id = "ORD-4821" |
| Parent-enfant | Hiérarchie reproduisant les appels réels | Order Service → DB Query |
Prérequis
Il est utile d'avoir déjà vu la notion de middleware HTTP (leçon sur les métriques applicatives) : l'instrumentation de traces fonctionne selon une logique similaire, autour du cycle de vie d'une requête.
Une fois cette anatomie comprise, une question technique se pose : comment un span "sait" qui est son parent d'un service à l'autre ?
C'est là qu'intervient la propagation de contexte, standardisée par le W3C sous forme d'un simple header HTTP, traceparent. Quand un service appelle un autre service, il transmet ce header ; le service appelé lit le trace_id et le span_id du parent pour rattacher correctement ses propres spans au bon endroit de l'arbre.
Maintenant, une distinction essentielle à ne pas manquer : OpenTelemetry n'est PAS Jaeger
OpenTelemetry est un standard ouvert, indépendant de tout fournisseur, qui définit comment instrumenter le code et comment transmettre les traces (via le protocole OTLP). Jaeger, lui, n'est qu'un backend de visualisation PARMI D'AUTRES (Tempo de Grafana Labs, Zipkin) qui sait recevoir et afficher ces traces.
Bonne pratique
Instrumente ton code avec le SDK OpenTelemetry générique plutôt qu'avec une bibliothèque spécifique à Jaeger. Si l'équipe décide plus tard de migrer vers Tempo ou un autre backend, il suffit de changer la configuration de l'exportateur, sans toucher une seule ligne de code applicatif.
Le rôle central, souvent sous-estimé, du Collector
Piège fréquent
Beaucoup de débutants configurent leur application pour envoyer directement ses traces vers Jaeger, sans passer par un Collector OpenTelemetry intermédiaire. Cela fonctionne en développement, mais complique fortement la migration future vers un autre backend ou l'ajout d'un second backend en parallèle (par exemple exporter aussi des métriques dérivées des spans, comme le montre l'exemple de cette leçon).
Le Collector centralise la réception de toutes les traces et peut les router vers plusieurs destinations en même temps, sans jamais modifier le code des applications elles-mêmes.
Tu as maintenant vu les trois piliers en profondeur : logs, métriques, traces. La prochaine leçon change d'angle en abordant une question différente : comment définir, chiffrer et suivre un objectif de fiabilité avec les SLI, SLO et SLA.
Commandes & code
Tracing distribué : OpenTelemetry et Jaeger
Anatomie d'une trace :
Trace ID: abc123 (identifie TOUTE la requête, de bout en bout)
│
├── Span "API Gateway" [0ms -> 320ms] (span racine)
│ ├── Span "Auth Service" [5ms -> 20ms] (span enfant)
│ └── Span "Order Service" [22ms -> 310ms] (span enfant)
│ ├── Span "DB Query" [30ms -> 150ms] (span petit-enfant)
│ └── Span "Payment API" [155ms -> 305ms] (span petit-enfant, appel externe)
│
-> chaque span a : un nom, un start/end time, des attributs (tags), et un parent (sauf la racine)# OpenTelemetry Python — instrumentation d'une app FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from fastapi import FastAPI
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="otel-collector:4317", insecure=True)))
trace.set_tracer_provider(provider)
app = FastAPI()
FastAPIInstrumentor.instrument_app(app) # instrumentation automatique de toutes les routes
tracer = trace.get_tracer(__name__)
@app.get("/orders/{order_id}")
def get_order(order_id: str):
with tracer.start_as_current_span("fetch_order_from_db") as span:
span.set_attribute("order.id", order_id) # attribut custom, visible dans Jaeger
order = fetch_from_db(order_id)
span.set_attribute("order.total", order["total"])
with tracer.start_as_current_span("call_payment_api") as span:
span.set_attribute("payment.provider", "stripe")
result = call_payment_api(order)
return order# docker-compose.yml — Jaeger tout-en-un pour du dev/démo (all-in-one, pas pour la prod à grande échelle)
services:
jaeger:
image: jaegertracing/all-in-one:1.58
ports:
- "16686:16686" # UI Jaeger
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver# otel-collector-config.yaml — le Collector centralise la réception ET le routage vers plusieurs backends
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch: {}
memory_limiter:
limit_mib: 512
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
prometheus:
endpoint: "0.0.0.0:8889" # exporte aussi des métriques dérivées des traces (span metrics)
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/jaeger]Context propagation : comment le trace_id voyage entre services (HTTP headers, standard W3C Trace Context)
Requête sortante ajoute automatiquement :
traceparent: 00-abc123def456-span789-01
^^ version ^^ trace-id ^^ parent-id ^^ flags
-> chaque service instrumenté lit ce header, rattache ses propres spans au même trace_id, et le propage à son tourRésumé
- Une trace = arbre de spans reliés par un
trace_idcommun ; chaque span a un parent (sauf la racine) et porte des attributs custom. - OpenTelemetry est le standard vendor-neutral (SDK + Collector) ; Jaeger n'est qu'UN backend possible de visualisation parmi d'autres (Tempo, Zipkin...).
- Le Collector centralise réception et export : il peut router les mêmes traces vers plusieurs backends, ou même en dériver des métriques (span metrics).
- La propagation de contexte (header
traceparent, standard W3C) est ce qui permet de relier des spans à travers des services et des langages différents.
Exercices pratiques
Mission : reconstruire l'arbre d'une commande capricieuse
Objectif : Instrumenter une route avec des spans OpenTelemetry pertinents et diagnostiquer une trace incomplète causée par une mauvaise propagation de contexte.
Contexte
Une commande met parfois 3 secondes à se valider, parfois 300 millisecondes. Dans Jaeger, certaines traces s'arrêtent net au niveau de l'API Gateway, sans qu'aucun span des services Order ou Payment n'apparaisse en dessous — alors que les logs de ces services montrent bien qu'ils ont bien été appelés durant l'incident.