Correlation IDs et traçabilité de bout en bout

TL;DR : Lorsque l'exécution traverse des threads, des files d'attente et le temps, les identifiants de corrélation sont le seul moyen fiable de préserver l'intégrité des histoires de logs.

Dans la partie 1 (→ Pourquoi les logs cessent d'être utiles dans les systèmes réels), nous avons vu comment les modèles de journalisation de base échouent en situation de concurrence et à grande échelle.

Dans la partie 2 (→ Le contexte utilisateur sans compromettre votre design), nous avons ajouté le contexte utilisateur à l'aide de MDC, sans polluer la logique métier.

Cette approche résout de nombreux problèmes réels de production – mais pas tous. Examinons ce qui se passe lorsque le contexte basé sur l'utilisateur ne suffit plus.

ID de corrélation

Le filtrage des logs par ID utilisateur est efficace dans de nombreuses situations, mais il existe des cas importants où cela ne fonctionne tout simplement pas.

Tout système utilisant l'authentification doit exposer au moins un point de terminaison ouvert, tel qu'un point de terminaison de connexion. Ce point de terminaison peut être appelé par des utilisateurs non authentifiés, ce qui signifie qu'il n'y a pas de userId disponible dans MDC.

Il existe également d'autres scénarios dans lesquels l'identité de l'utilisateur n'existe pas ou n'est pas utile :

  • utilisateurs invités,
  • tâches en arrière-plan,
  • tâches planifiées,
  • traitement asynchrone.

Examinons un exemple concret.

❌ Problème : l'exécution asynchrone brise le scénario

Un utilisateur invité commande un article et applique éventuellement un coupon de réduction. Le traitement du paiement s'effectue de manière asynchrone.

Les journaux ressemblent à ceci :

2025-10-25 19:32:25,446 INFO 305188 [t=Thread-2] demo.PrintingDemo Guest used coupon code = ***** : [u=] 2025-10-25 19:32:25,451 INFO 305188 [t=Thread-2] demo.PrintingDemo Guest started payment process : [u=] 2025-10-25 19:32:25,454 INFO 305188 [t=Thread-1] demo.PrintingDemo Guest skipped coupon code : [u=] 2025-10-25 19:32:25,461 INFO 305188 [t=Thread-3] demo.PrintingDemo Guest skipped coupon code : [u=] 2025-10-25 19:32:25,548 INFO 305188 [t=Thread-1] demo.PrintingDemo Guest started payment process : [u=] 2025-10-25 19:32:25,567 INFO 305188 [t=Thread-3] demo.PrintingDemo Guest started payment process : [u=] 2025-10-25 19:32:25,772 INFO 305188 [t=Async--6] demo.PrintingDemo Guest aborted payment : [u=] 2025-10-25 19:32:25,967 INFO 305188 [t=Async--7] demo.PrintingDemo Guest finished payment : [u=] 2025-10-25 19:32:26,231 INFO 305188 [t=Async--8] demo.PrintingDemo Guest finished payment : [u=]

La question à laquelle nous devons répondre est simple : le client qui a utilisé le code promotionnel a-t-il finalisé le paiement ?

À partir de ces seuls journaux, il est impossible de le dire. L'ID de thread ne sera pas utile ici, car la finalisation du paiement s'exécute sur un autre pool de threads. Le PID ne sera pas utile non plus. L'ID utilisateur n'existe pas.

Nous avons tous les événements, mais aucun moyen fiable de déterminer lesquels vont ensemble.

✅ Solution : inventer un identifiant d'exécution

Si l'ID utilisateur, l'ID de thread et le PID sont insuffisants, que pouvons-nous utiliser d'autre ? La réponse est étonnamment simple : nous n'avons pas besoin de réutiliser un identifiant existant – nous pouvons en créer un.

Toute valeur localement unique fera l'affaire : une chaîne aléatoire, un nombre aléatoire ou un UUID. Comme cet identifiant est utilisé pour montrer les relations entre les entrées de journal, nous l'appelons un ID de corrélation.

Un identifiant de corrélation représente une seule exécution, indépendamment de :

  • combien de threads sont impliqués,
  • si l'exécution est synchrone ou asynchrone,
  • ou combien de temps cela prend.

Sécurité vs lisibilité

Le choix du bon format d'identifiant de corrélation implique un compromis entre l'unicité (sécurité) et la lisibilité (utilisabilité opérationnelle).

Un UUID standard est l'option la plus sûre :

4d2108d1-35a6-41a3-9ed2-1b146c78bb9c

Il garantit l'unicité, même entre les systèmes, mais il est long et difficile à manipuler. Pour réduire sa longueur, on peut envisager l'encodage Base64 :

TSEI0TWmQaOe0hsUbHi7nA==

Cependant, le Base64 introduit une ambiguïté visuelle (0, O, l, I) et est sujet aux erreurs lors d'une copie manuelle.

Un compromis courant est le Base58, qui évite ces caractères et est conçu pour un usage humain (célèbre pour son utilisation dans Bitcoin). Si une unicité globale absolue n'est pas requise, un identifiant court en Base58 est souvent suffisant : ‘aMXaBGD‘.

Stratégie à double ID

Si vous avez besoin à la fois de sécurité et d'une bonne ergonomie, une approche pratique consiste à journaliser deux identifiants :

  • un identifiant court et lisible par l'homme (pour la recherche et la communication),
  • un UUID complet (pour une certitude forensique).

Dans le cas rare d'une collision, vous pouvez filtrer les journaux par l'ID court, puis lever l'ambiguïté à l'aide de l'UUID.

Intégration de l'ID de corrélation dans le MDC

Tout comme l'ID utilisateur, l'ID de corrélation doit être stocké dans le MDC au début de l'exécution. Le modèle de journalisation est ensuite étendu comme suit :

%d{ISO8601} %-5level ${PID} [t=%thread] %-48logger{48} [c=%X{correlationId}] %msg : [u=%X{userId}] %n%throwable | +--- correlation ID …montre la relation entre les lignes de log

Ici :

  • %X{correlationId} lit la valeur depuis MDC,
  • chaque ligne de log porte désormais une identité d'exécution.

Exemple asynchrone complet

Avec les identifiants de corrélation en place, l'exemple asynchrone précédent devient lisible :

2025-10-25 21:05:49,293 INFO 317606 [t=Thread-1] demo.PrintingDemo [c=FB6bA3f] Guest used coupon code = ***** : [u=] 2025-10-25 21:05:49,375 INFO 317606 [t=Thread-1] demo.PrintingDemo [c=FB6bA3f] Guest started payment process : [u=] 2025-10-25 21:05:51,203 INFO 317606 [t=Async--5] demo.PrintingDemo [c=FB6bA3f] Guest aborted payment : [u=]

Filtrer les logs par identifiant de corrélation répond immédiatement à notre question initiale : L'invité qui a utilisé le coupon a abandonné le paiement.

Aucune supposition et aucune reconstruction manuelle.

ID de corrélation au-delà des journaux

Les identifiants de corrélation deviennent encore plus puissants lorsqu'ils quittent le système de journalisation.

Si nous incluons l'ID de corrélation dans les réponses d'erreur de l'API, le rapport de bug d'un utilisateur pourrait contenir quelque chose comme ceci :

{ "timestamp": "2025-10-25T21:43:56Z", "message": "Unable to process order with negative price: -64", "userId": "ferdynand@oo.pl", "error": "Bad request", "status": 400, "method": "POST", "path": "/api/orders", "correlationId": "pHVAVwv" }

Le débogage devient alors simple. Il suffit de rechercher pHVAVwv dans les journaux pour voir immédiatement l'exécution complète :

2025-10-25 21:43:56,210 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Price of item = 64 : [u=ferdynand@oo.pl] 2025-10-25 21:43:56,234 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] En raison de pénuries, la quantité a été réduite de 3 articles : [u=ferdynand@oo.pl] 2025-10-25 21:43:56,234 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Items in order = -1 : [u=ferdynand@oo.pl] 2025-10-25 21:43:56,249 ERROR 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Requête malformée : [u=ferdynand@oo.pljava.lang.IllegalStateException: Unable to process order with negative price: -64 at demo.PrintingDemo.runSequence(PrintingDemo.java:92) at demo.PrintingDemo.lambda$onApplicationReady$0(PrintingDemo.java:66) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1095) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:619) at java.base/java.lang.Thread.run(Thread.java:1447) 2025-10-25 21:43:56,252 INFO 326565 [t=Thread-1] demo.PrintingDemo [c=pHVAVwv] Request body: {"quantity":2} : [u=ferdynand@oo.pl]

À ce stade, aucun travail d'enquête n'est nécessaire.

Aller au-delà des utilisateurs techniques

Les utilisateurs non techniques signalent souvent les problèmes en envoyant des captures d'écran. Ils peuvent capturer un message d'erreur plutôt qu'une réponse HTTP.

correlation IDs 1

Il serait plutôt difficile d'identifier la cause première, et encore plus de le faire rapidement. Nous avons déjà enrichi les messages de réponse avec un identifiant de corrélation (correlation ID). Nous pouvons ajouter cette information à la réponse, car ce n'est pas un secret. Et si ce n'est pas un secret dans la réponse, ce n'est un secret nulle part ailleurs. Par conséquent, pourquoi ne pas la placer directement dans la snackbar d'erreur ?

Si l'ID de corrélation est visible dans ce message, même une capture d'écran suffit pour localiser les journaux concernés. C'est aussi pourquoi la lisibilité est importante.

Pour démontrer l'importance de choisir un format d'ID lisible, examinons les problèmes qui surviennent si nous avions utilisé un identifiant complexe, tel qu'un UUID encodé en Base64. L'ambiguïté visuelle rend la copie sujette aux erreurs :

Un court identifiant Base58 comme WpJ9ZWr est facile à lire, à copier et à rechercher – contrairement à un identifiant long et visuellement ambigu.

En prenant l'ID de corrélation à partir de la capture d'écran de la snackbar claire (WpJ9ZWr) et en recherchant dans notre système de logs, nous pouvons rapidement trouver les détails pertinents de la requête :

2025-10-25 22:19:52,938 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Price of item = 3 : [u=mr.1337@pwnd.it] 2025-10-25 22:19:52,961 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] En raison de pénuries, la quantité a été réduite de 3 articles : [u=mr.1337@pwnd.it] 2025-10-25 22:19:52,961 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Items in order = -1 : [u=mr.1337@pwnd.it] 2025-10-25 22:19:53,150 ERROR 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Requête malformée : [u=mr.1337@pwnd.itjava.lang.IllegalStateException: Unable to process order with negative price: -36 at demo.PrintingDemo.runSequence(PrintingDemo.java:92) at demo.PrintingDemo.lambda$onApplicationReady$0(PrintingDemo.java:66) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1095) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:619) at java.base/java.lang.Thread.run(Thread.java:1447) 2025-10-25 22:19:53,153 INFO 338180 [t=Thread-1] demo.PrintingDemo [c=WpJ9ZWr] Request body: {"quantity":2} : [u=mr.1337@pwnd.it]

Et si personne n'avait vu la défaillance ?

Il arrive que des défaillances se produisent sans que l'utilisateur ne s'en aperçoive – par exemple, dans les tâches planifiées. Dans ces cas, des alertes ou des notifications sont généralement déclenchées.

Si vous disposez déjà d'un système d'alerte, le même principe s'applique : chaque alerte doit inclure un identifiant de corrélation. Ainsi, une alerte mène directement à l'exécution pertinente dans les journaux.

Conclusion

Cette série d'articles ne fait qu'effleurer la surface de la simplification du travail de maintenance.

Dans les systèmes plus complexes, les identifiants de corrélation doivent être propagés entre les services, et des outils d'observabilité dédiés peuvent représenter un meilleur investissement.

Cependant, l'expérience montre que la grande majorité des systèmes de production sont encore des services uniques ou des déploiements simples. Dans ces cas, quelques améliorations mineures et bien ciblées – le contexte utilisateur, le MDC et les identifiants de corrélation – peuvent réduire considérablement le temps consacré à la compréhension des défaillances.

Parfois, de simples astuces suffisent vraiment.