Correlation ID, Trace ID, Span ID : comprendre le squelette de l'observabilité distribuée

"Le bug est en prod, l'utilisateur a renvoyé un identifiant de requête dans un ticket support, et tu as 8 microservices, un broker Kafka et un webhook PSP à inspecter. Par où tu commences ?"

Cette question, n'importe quel développeur backend travaillant sur des systèmes distribués l'a déjà rencontrée.

Et c'est précisément pour répondre à ce problème qu'existent les notions de correlation ID, trace ID, span ID et, plus largement, les mécanismes d'observabilité distribuée.

Pendant longtemps, j'ai moi-même considéré le correlationId et le traceId comme deux variantes du même concept. Mais en construisant une petite librairie Spring Boot d'observabilité — capable de corréler les requêtes HTTP, les appels inter-services et même les requêtes SQL Hibernate — je me suis rendu compte qu'ils ne résolvaient pas exactement le même problème.

Cet article propose donc une exploration pragmatique de ces concepts, en partant d'un besoin très concret : comment garder un fil narratif cohérent dans un système où chaque service ne voit qu'une fraction de l'histoire ?

1. Le problème : dans un système distribué, les logs seuls ne racontent plus l'histoire

Dans un monolithe classique, une requête utilisateur produit une suite d'événements dans :

  • un seul processus,
  • une seule machine,
  • un seul fichier de logs.

Avec un peu de patience, on peut souvent reconstituer l'exécution :

  • via le thread name,
  • les timestamps,
  • ou quelques logs métier.

Mais dans un système distribué, la même action utilisateur peut traverser :

  • plusieurs services,
  • plusieurs bases de données,
  • plusieurs machines,
  • parfois des brokers,
  • parfois des systèmes tiers.

Exemple :

flowchart TD Client --> OS[order-service] OS --> PS[payment-service] PS --> NS[notification-service] NS --> DB[(PostgreSQL)] DB --> PSP[PSP externe] PSP --> WH[Webhook retour]

Sans mécanisme de corrélation explicite :

  • impossible de reconstituer le chemin complet d'une requête,
  • impossible de relier une requête SQL lente à l'appel HTTP qui l'a déclenchée,
  • impossible de savoir quel workflow métier a produit tel événement.

Les timestamps ne suffisent pas : deux utilisateurs peuvent appeler simultanément le même endpoint et produire des logs totalement entrelacés.

Il faut donc introduire des identifiants capables de traverser les frontières techniques.

2. Première confusion classique : correlation ID et trace ID ne sont pas la même chose

Pendant longtemps, beaucoup d'équipes ont utilisé un simple :

X-Correlation-ID: abc123

propagé de service en service.

Puis OpenTelemetry, Zipkin, Jaeger et les standards W3C sont arrivés avec :

  • traceId,
  • spanId,
  • traceparent.

À première vue, les deux semblent faire la même chose :

identifier une requête à travers plusieurs services.

Mais en réalité : ils opèrent à deux niveaux différents.

3. Le trace ID : la continuité technique

Le traceId appartient au monde du tracing distribué.

Il représente :

une exécution technique continue.

Exemple :

flowchart LR Client -->|HTTP| OS[order-service] OS -->|HTTP| PS[payment-service] PS -->|HTTP| IS[inventory-service]

Dans cette chaîne :

  • tous les services partagent le même traceId,
  • chaque opération locale possède son propre spanId.

Exemple simplifié :

flowchart LR subgraph trace["traceId = 4bf92f3577b34da6a3ce929d0e0e4736"] OS["order-service<br/>spanId = a1b2c3"] PS["payment-service<br/>spanId = d4e5f6"] IS["inventory-service<br/>spanId = g7h8i9"] OS --> PS PS --> IS end

Le traceId permet donc :

  • de reconstruire l'arbre technique d'une exécution,
  • de mesurer les durées,
  • d'afficher des waterfalls,
  • de visualiser les dépendances inter-services.

C'est exactement ce que font :

  • Jaeger,
  • Tempo,
  • Datadog APM,
  • Zipkin.

4. Le span ID : l'unité de travail locale

Un span représente une opération locale :

  • une requête HTTP,
  • un appel SQL,
  • un appel sortant,
  • un traitement métier.

Chaque span possède :

  • un spanId,
  • un parentSpanId,
  • une durée,
  • des métadonnées.

Exemple :

flowchart TD subgraph trace["traceId = TRACE-123"] A["span A<br/>POST /orders"] B["span B<br/>GET /payments"] C["span C<br/>SQL SELECT payment"] A --> B B --> C end

Le traceId relie toute la trace. Les spanId structurent l'arbre interne.

5. Le vrai rôle du correlation ID : la continuité métier

C'est ici que la subtilité apparaît.

Le traceId suit une exécution technique continue.

Mais un workflow métier réel dépasse souvent une seule exécution technique.

Prenons un paiement avec un PSP externe :

flowchart LR Client --> OS[order-service] OS --> PSP[PSP externe]

À ce moment :

  • le traceId OpenTelemetry existe,
  • tout est cohérent techniquement.

Mais quelques secondes plus tard :

flowchart LR PSP --> Webhook[webhook] Webhook --> PS[payment-service]

Une nouvelle requête HTTP démarre.

Donc :

  • nouveau thread,
  • nouvelle exécution,
  • nouveau traceId.

Et c'est parfaitement normal.

Le PSP :

  • ne participe pas à ton runtime,
  • ne propage pas forcément ton contexte OpenTelemetry,
  • possède sa propre infrastructure.

Donc :

flowchart LR subgraph traceA["Trace A"] Client --> OS[order-service] OS --> PSP[PSP] end

et :

flowchart LR subgraph traceB["Trace B"] PSP2[PSP] --> WH[webhook] WH --> PS[payment-service] end

sont deux traces techniques distinctes.

Pourtant : métierment, il s'agit toujours du même paiement.

C'est précisément là qu'intervient le correlationId.

6. Le correlation ID : le fil rouge métier

Le correlationId représente :

l'identifiant global du workflow métier.

Contrairement au traceId, il :

  • peut survivre à plusieurs requêtes,
  • plusieurs traces,
  • plusieurs systèmes,
  • plusieurs moments temporels.

Exemple :

correlationId = PAY-2026-XYZ

Puis :

flowchart TD subgraph A["Trace A"] ClientA[Client] --> OSA[order-service] OSA --> PSPA[PSP] end subgraph B["Trace B"] PSPB[PSP] --> WHB[webhook] WHB --> PSB[payment-service] end subgraph C["Trace C"] PSC[payment-service] --> KafkaC[Kafka] KafkaC --> NSC[notification-service] end subgraph D["Trace D"] NSD[notification-service] --> SMTPD[SMTP] end A -.->|correlationId = PAY-2026-XYZ| B B -.->|correlationId = PAY-2026-XYZ| C C -.->|correlationId = PAY-2026-XYZ| D

Tous ces événements appartiennent au même workflow métier :

correlationId = PAY-2026-XYZ

Le traceId suit le runtime. Le correlationId suit le métier.

7. Pourquoi les PSP possèdent des champs "reference"

C'est ici qu'on comprend enfin le vrai rôle de champs comme :

  • merchantReference,
  • externalReference,
  • clientReference,
  • orderReference.

Ils ne servent pas uniquement à "mettre un numéro de commande".

Ils servent surtout à :

corréler un workflow métier entre plusieurs systèmes indépendants.

Exemple :

Appel initial vers le PSP

{
  "amount": 100,
  "merchantReference": "PAY-2026-XYZ"
}

Puis plus tard :

Webhook retour

{
  "merchantReference": "PAY-2026-XYZ",
  "status": "PAID"
}

Ton système peut alors :

  • retrouver le workflow métier,
  • rattacher une nouvelle trace technique,
  • reconstruire toute l'histoire du paiement.

C'est exactement ce que permet le correlationId.

8. Notre starter : faire coexister corrélation métier et tracing technique

Notre starter Spring Boot repose sur une idée simple :

flowchart LR T[traceId]:::tech -->|porte| TC[continuité technique] C[correlationId]:::biz -->|porte| BC[continuité métier] classDef tech fill:#dbeafe,stroke:#1e40af classDef biz fill:#fef3c7,stroke:#b45309

Chaque requête transporte donc :

  • correlationId,
  • traceId,
  • spanId,
  • parentSpanId.

Exemple :

public record TraceContext(
    String correlationId,
    String traceId,
    String spanId,
    String parentSpanId,
    String serviceName
) {}

9. Le filtre HTTP : point d'entrée du contexte

Un OncePerRequestFilter :

  • lit les en-têtes entrants,
  • génère les IDs manquants,
  • initialise le MDC.

Pseudo-code :

String correlationId = request.getHeader("X-Correlation-ID");

if(correlationId == null) {
    correlationId = CorrelationIdGenerator.generate();
}

String traceId =
    extractOrGenerateTraceId(request);

MDC.put("correlationId", correlationId);
MDC.put("traceId", traceId);

try {
    chain.doFilter(request, response);
} finally {
    MDC.clear();
}

Une fois le MDC initialisé : tous les logs produits pendant la requête héritent automatiquement du contexte.

Sans toucher au métier.

10. Propagation HTTP sortante

Les appels inter-services propagent automatiquement :

  • X-Correlation-ID,
  • traceparent.

Exemple :

request.getHeaders()
    .add("X-Correlation-ID", context.correlationId());

request.getHeaders()
    .add("traceparent", TraceParent.format(context));

Le service distant :

  • récupère le même correlationId,
  • récupère le même traceId,
  • génère un nouveau spanId.

11. Corrélation SQL : le chaînon souvent oublié

La partie la plus intéressante de notre starter est probablement l'instrumentation SQL.

Grâce à StatementInspector Hibernate, nous injectons des commentaires SQL :

/* corr_id=PAY-2026-XYZ trace_id=4bf92f... service=payment-service */
SELECT * FROM payment WHERE order_id = ?

Ces commentaires :

  • n'affectent pas l'exécution SQL,

  • mais apparaissent dans :

    • les slow query logs,
    • PostgreSQL,
    • pg_stat_statements,
    • les outils DBA.

Résultat : un DBA peut relier une requête lente :

  • à un service,
  • à un workflow métier,
  • à une requête HTTP,
  • à un utilisateur.

C'est extrêmement puissant.

12. Pourquoi faire ça alors qu'OpenTelemetry existe déjà ?

Parce qu'OpenTelemetry résout principalement :

  • le tracing technique,
  • la propagation,
  • les spans,
  • l'export.

Mais il ne résout pas automatiquement :

  • la corrélation métier,
  • l'exposition des IDs dans les réponses,
  • les commentaires SQL,
  • les workflows PSP/webhooks,
  • la visibilité simple dans les logs.

Notre starter peut fonctionner :

  • seul,
  • ou comme complément léger à OpenTelemetry.

Le positionnement n'est donc pas :

remplacer OpenTelemetry

mais plutôt :

rendre le contexte technique et métier immédiatement exploitable dans les logs, SQL et workflows distribués.

13. Résumé

Concept Rôle
traceId Identifiant technique distribué d'une exécution continue
spanId Identifiant local d'une unité de travail
correlationId Identifiant métier d'un workflow global
MDC Rend les IDs visibles dans tous les logs
SQL comments Relient les logs DB au workflow métier

Au fond, toute l'observabilité distribuée repose sur une seule idée :

garder un fil narratif cohérent dans un système fragmenté.

Le traceId raconte l'histoire technique. Le correlationId raconte l'histoire métier.

Et c'est précisément lorsqu'on combine les deux que les systèmes distribués deviennent réellement observables.