Astuces simples pour améliorer la maintenabilité de votre application (Partie 2) : le contexte utilisateur sans compromettre votre design
Le contexte utilisateur sans compromettre votre design
En bref : transmettre des identifiants utilisateur dans votre code fonctionne – mais le MDC permet à vos journaux de porter le contexte utilisateur sans polluer la logique métier.
Dans la partie 1 de cette série (→ Pourquoi les logs cessent d'être utiles dans les systèmes réels), nous avons constaté comment les modèles de journalisation de base se brisent dès que la concurrence, les délais et plusieurs utilisateurs entrent en jeu.
À ce stade, les journaux étaient encore techniquement corrects, mais ils ne permettaient plus de reconstituer ce qui était arrivé à un utilisateur ou à une requête en particulier. Passons à l'étape suivante.
ID utilisateur comme contexte de diagnostic
Tout d'abord, considérez l'utilisateur de l'application. Il n'est peut-être pas notre collègue. Il ne travaille peut-être pas dans notre entreprise – ou n'est même peut-être pas joignable. Supposons qu'il signale un problème dans un bug tracker et inclue son identifiant système. Cet identifiant peut être :
- un nom d'utilisateur,
- un numéro de système,
- ou une adresse e-mail.
Dans notre exemple, il s'agit d'une adresse e-mail : mamian@zoo.gov.eu
Pour exploiter ces informations, nous avons besoin d'un moyen de filtrer les journaux par identifiant utilisateur.
Si le code problématique ne peut être déclenché que par un utilisateur authentifié, nous sommes déjà plus proches de l'objectif. Dès qu'un utilisateur se connecte et est authentifié, tout ce qui se produit est exécuté dans son contexte.
Alors, essayons de l'utiliser.
❌ Problème : transmettre userId partout
L'approche la plus simple consiste à transmettre userId explicitement et incluez-le dans chaque instruction de journalisation :
public void modifyOrder(String userId) { try { var priceOfItem = getPriceOfItem(userId); var quantity = getQuantity(userId); var totalPrice = calculateTotalPrice(userId, priceOfItem, quantity); updateWith(totalPrice); } catch (OrderException e) { log.error("Failure: user (id = {})", userId); } } private int getPriceOfItem(String userId) { var priceOfItem = repository.getPriceOfItem(userId); logger.info("User (id = {}) called: Price of item = {}", userId, priceOfItem); return priceOfItem; } private int getQuantity(String userId) { var quantity = repository.getQuantity(userId); logger.info("User (id = {}) called: Items in order = [{}]", userId, quantity); return quantity; } private int calculateTotalPrice(String userId, int priceOfItem, int quantity) { var totalPrice = calculator.calculate(userId, price, quantity); logger.info("User (id = {}) called: Total price = {}", userId, totalPrice); return totalPrice; }
Cette approche fonctionne, mais elle devient rapidement problématique.
Réussir userId à chaque méthode devient une mauvaise conception :
- et si nous souhaitions plus tard remplacer unChaîne e-mail avec un UUID de base de données ou unIdentifiant utilisateur objet valeur ?
- que se passe-t-il si nous oublions de le transmettre à une méthode, ou d'enregistrer son log ?
- que se passe-t-il si les préoccupations liées à la journalisation commencent à s'infiltrer dans une logique métier par ailleurs propre ?
En pratique, cette solution devient étonnamment fastidieuse très rapidement.
✅ Solution : Mapped Diagnostic Context (MDC)
Mapped Diagnostic Context (MDC) est un utilitaire petit mais puissant intégré à toutes les bibliothèques de journalisation Java populaires. Il nous permet de stocker des valeurs de diagnostic dans une mémoire locale au thread, distincte pour chaque thread d'exécution.
L'idée est simple :
- lorsque l'exécution démarre, nous plaçons des données de diagnostic utiles dans le MDC,
- lorsque l'exécution se termine, nous nettoyons le MDC.
Comme MDC est local au thread :
- les données de diagnostic d'une exécution ne peuvent pas fuiter vers une autre,
- mais oublier de le nettoyer est dangereux, car les threads sont réutilisés.
Où MDC s'intègre naturellement
Dans les applications basées sur Spring, la limite d'exécution la plus courante est la requête HTTP. Cela fait du traitement des requêtes un endroit idéal pour initialiser le MDC au début et le nettoyer lors de l'envoi de la réponse.
Un modèle similaire s'applique à :
- tâches planifiées,
- consommateurs de file d'attente,
- ou toute exécution avec un début et une fin clairs.
Voici un exemple qui gère correctement le cycle de vie du MDC :
- Initialisez le filtre qui est ensuite automatiquement injecté dans chaque flux d'appels REST API.
- Collecter des informations sur l'utilisateur connecté.
- Placez l'utilisateur connecté dans le MDC au début, puis nettoyez le MDC à la fin.
- Le enfin Le bloc est essentiel ici – il garantit que le MDC est toujours nettoyé, même lorsqu'une exception se produit.
@Component public class GlobalLoggingRequestFilter extends OncePerRequestFilter { private final AuthenticationService authService; public GlobalLoggingRequestFilter(AuthenticationService authService) { this.authService = authService; } private Optional<String> extractUserIdFrom(HttpServletRequest request) { // In a real application, this service call would parse JWT // or look up session data based on the request. return authService.extractUserId(request); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // init MDC with user ID extractUserIdFrom(request).ifPresent(userId -> MDC.put("userId", userId)); // let the business logic be executed filterChain.doFilter(request, response); } finally { // finish and clean MDC context MDC.clear(); } } }
❌ Problème : couplage du MDC avec le code métier
À ce stade, l'ID utilisateur est disponible via MDC, nous n'avons donc plus besoin de le transmettre via les paramètres de méthode.
Cependant, si nous accédons manuellement au MDC dans chaque instruction de journalisation, nous introduisons une nouvelle forme de couplage :
public void modifyOrder() { try { var priceOfItem = getPriceOfItem(); var quantity = getQuantity(); var totalPrice = calculateTotalPrice(priceOfItem, quantity); updateWith(totalPrice); } catch (OrderException e) { log.error("Failure: user (id = {})", MDC.get("userId")); } } private int getPriceOfItem() { var priceOfItem = repository.getPriceOfItem(); logger.info("User (id = {}) called: Price of item = {}", MDC.get("userId"), priceOfItem); return priceOfItem; } private int getQuantity() { var quantity = repository.getQuantity(); logger.info("User (id = {}) called: Items in order = [{}]", MDC.get("userId"), quantity); return quantity; } private int calculateTotalPrice(int priceOfItem, int quantity) { var totalPrice = calculator.calculate(price, quantity); logger.info("User (id = {}) called: Total price = {}", MDC.get("userId"), totalPrice); return totalPrice; }
Cela pollue encore le code de journalisation, couple la logique métier à l'infrastructure de journalisation et exige de la discipline partout.
✅ Solution : laisser le logger lire le MDC pour nous
Heureusement, Logback peut lire automatiquement les valeurs MDC. Il suffit de mettre à jour le modèle de journalisation :
%d{ISO8601} %-5level ${PID} [t=%thread] %-48logger{48} %msg : [u=%X{userId}] %n%throwable | | | +--- accéder à une variable depuis le MDC | …utilise le spécificateur de conversion %X | …accès à userId dans notre cas | +--- séparateur visuel
Ici, %X{userId} indique à Logback de lire le userId la valeur du MDC et l'ajouter à chaque ligne de journal. Aucune modification des appels de journalisation n'est requise.
Un code métier plus propre, des logs plus riches
Avec cette configuration en place, le code de l'application redevient propre :
public void modifyOrder() { try { var priceOfItem = getPriceOfItem(); var quantity = getQuantity(); var totalPrice = calculateTotalPrice(priceOfItem, quantity); updateWith(totalPrice); } catch (OrderException e) { log.error("Failure"); } } private int getPriceOfItem() { var priceOfItem = repository.getPriceOfItem(userId); logger.info("Price of item = {}", priceOfItem); return priceOfItem; } private int getQuantity() { var quantity = repository.getQuantity(userId); logger.info("Items in order = [{}]", quantity); return quantity; } private int calculateTotalPrice(int priceOfItem, int quantity) { var totalPrice = calculator.calculate(userId, price, quantity); logger.info("Total price = {}", totalPrice); return totalPrice; }
Et les journaux incluent automatiquement le contexte utilisateur :
2025-10-20 00:29:11,300 INFO 214731 [t=Thread-1] demo.PrintingDemo Price of item = 17 : [u=ferdynand@oo.pl2025-10-20 00:29:11,356 INFO 214731 [t=Thread-1] demo.PrintingDemo Items in order = [3] : [u=ferdynand@oo.pl] 2025-10-20 00:29:11,441 INFO 214731 [t=Thread-1] demo.PrintingDemo Total price = 51 : [u=ferdynand@oo.pl] 2025-10-20 00:29:14,540 INFO 214731 [t=Thread-1] demo.PrintingDemo Price of item = 76 : [u=mamian@zoo.gov.eu2025-10-20 00:29:14,682 INFO 214731 [t=Thread-1] demo.PrintingDemo Items in order = [2] : [u=mamian@zoo.gov.eu] 2025-10-20 00:29:14,810 INFO 214731 [t=Thread-1] demo.PrintingDemo Total price = -152 : [u=mamian@zoo.gov.eu2025-10-20 00:29:16,884 INFO 214731 [t=Thread-1] demo.PrintingDemo Price of item = 81 : [u=mr.1337@pwnd.it2025-10-20 00:29:16,959 INFO 214731 [t=Thread-1] demo.PrintingDemo Items in order = [1] : [u=mr.1337@pwnd.it] 2025-10-20 00:29:16,979 INFO 214731 [t=Thread-1] demo.PrintingDemo Total price = 81 : [u=mr.1337@pwnd.it]
Le filtrage des journaux par ID utilisateur révèle immédiatement l'exécution problématique :
2025-10-20 00:29:14,540 INFO 214731 [t=Thread-1] demo.PrintingDemo Price of item = 76 : [u=mamian@zoo.gov.eu2025-10-20 00:29:14,682 INFO 214731 [t=Thread-1] demo.PrintingDemo Items in order = [2] : [u=mamian@zoo.gov.eu] 2025-10-20 00:29:14,810 INFO 214731 [t=Thread-1] demo.PrintingDemo Total price = -152 : [u=mamian@zoo.gov.eu]
Quelque chose ne va clairement pas – et désormais, il est facile de le repérer.
ID utilisateur vs. RGPD
Selon le secteur, la journalisation d'un identifiant utilisateur peut être restreinte par le RGPD ou d'autres réglementations.
Dans de tels cas, vous pouvez toujours consigner des métadonnées non identifiantes, telles que :
- user role,
- langue sélectionnée,
- pays,
- mode UI,
- type d'appareil.
Par exemple, au lieu de :
2025-10-20 00:29:11,441 INFO 214731 [t=Thread-1] demo.PrintingDemo Total price = 51 : [u=ferdynand@oo.pl]
vous pourriez journaliser :
2025-10-20 00:29:11,441 INFO 214731 [t=Thread-1] demo.PrintingDemo Total price = 51 : [role=CUSTOMER,lang=pl_PL,lc=pl,mode=DARK,dvc=MOBILE]
En pratique, cela fournit généralement suffisamment de contexte pour comprendre le problème, sans identifier une personne spécifique.
Où cela nous mène – et où cela ne nous mène pas
Le filtrage des logs par ID utilisateur résout de nombreux problèmes de production réels. Cela nous permet de :
- corréler les événements pour un utilisateur unique,
- identifier rapidement un comportement incorrect,
- réduire le temps consacré à la reconstruction manuelle des chemins d'exécution.
Cependant, cette approche présente encore des limites. Que se passe-t-il lorsque :
- les utilisateurs ne sont pas authentifiés,
- l'exécution s'étend sur plusieurs threads,
- le travail est-il effectué de manière asynchrone ?
Améliorons encore notre solution, d'accord ?
Prochaines étapes
→ Partie 3 : Identifiants de corrélation et traçabilité de bout en bout
Contactez-nous en cas de questions !
arrow_circle_right ARTICLES RECOMMANDÉS