Retour au cours

infra / monitoring-observabilite

Tracing distribué : OpenTelemetry et Jaeger

Leçon 91 exercice

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émentRôleExemple
Trace IDIdentifie toute la requête de bout en boutabc123
SpanUne étape unique avec début/fin"DB Query" [30ms → 150ms]
AttributMétadonnée custom attachée à un spanorder.id = "ORD-4821"
Parent-enfantHiérarchie reproduisant les appels réelsOrder 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

text
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)
python
# 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
yaml
# 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
yaml
# 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]
text
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 tour

Résumé

  • Une trace = arbre de spans reliés par un trace_id commun ; 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

1 disponible
1

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.

Résoudre l’exercice →