
Utilisation de MDC (Mapped Diagnostic Context) pour le suivi des requêtes
Apprenez à utiliser le Mapped Diagnostic Context (MDC) pour enrichir vos logs Spring Boot avec des informations contextuelles et suivre facilement le parcours d'une requête.
Qu'est-ce que le MDC (Mapped Diagnostic Context) ?
Le Mapped Diagnostic Context (MDC) est un concept proposé par plusieurs frameworks de logging Java (notamment Logback et Log4j) pour enrichir les messages de log avec des informations contextuelles. Il s'agit essentiellement d'une map associée au thread courant (thread-local). Vous pouvez y stocker des paires clé-valeur qui seront ensuite automatiquement disponibles pour être incluses dans chaque message de log généré par ce thread.
L'intérêt principal du MDC est de pouvoir ajouter des informations spécifiques à une transaction, une requête utilisateur, ou une session, sans avoir à les passer explicitement comme arguments à chaque appel de méthode de logging. Une fois les informations placées dans le MDC, le framework de logging peut les récupérer et les intégrer dans le format de log configuré.
Dans le contexte d'une application web comme celles construites avec Spring Boot, le MDC est particulièrement puissant pour suivre le parcours d'une requête HTTP spécifique à travers les différentes couches de l'application (contrôleur, service, repository) et même à travers des appels asynchrones (avec quelques précautions).
Le problème : Corréler les logs dans un environnement concurrent
Imaginez un serveur web traitant des centaines de requêtes simultanément. Chaque requête est généralement gérée par un thread différent (issu d'un pool de threads). Si plusieurs requêtes exécutent le même code et génèrent des logs, comment savoir quels messages de log appartiennent à quelle requête initiale ? Sans contexte supplémentaire, il est très difficile de reconstituer le flux d'une seule transaction en regardant simplement un flot de logs entremêlés.
C'est là que le MDC brille. Au début du traitement d'une requête, on peut générer un identifiant unique pour cette requête (Request ID) et le placer dans le MDC du thread courant. Ensuite, tous les logs émis par ce thread pendant le traitement de la requête pourront inclure cet ID. En filtrant les logs par cet ID unique, on peut isoler tous les messages relatifs à cette requête spécifique, quel que soit l'ordre dans lequel ils apparaissent dans le fichier de log global.
Outre l'ID de requête, on peut ajouter d'autres informations utiles dans le MDC, comme l'identifiant de l'utilisateur connecté, l'adresse IP du client, l'ID de session, etc., pour enrichir davantage le contexte de chaque message de log.
Implémentation du MDC dans Spring Boot pour les requêtes Web
Le moyen le plus courant et efficace d'implémenter le MDC pour le suivi des requêtes web dans Spring Boot est d'utiliser un Filtre Servlet (pour les applications MVC traditionnelles) ou un WebFilter (pour les applications réactives WebFlux).
Ce filtre intercepte chaque requête entrante, place les informations contextuelles souhaitées dans le MDC, laisse la requête continuer son traitement normal, puis, crucialement, nettoie le MDC une fois le traitement de la requête terminé (dans un bloc `finally` ou équivalent). Ce nettoyage est essentiel car les threads du pool sont réutilisés pour d'autres requêtes, et il ne faut pas que les informations d'une requête "contaminent" les logs d'une requête ultérieure traitée par le même thread.
Exemple avec un Filtre Servlet (Spring MVC) :
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.UUID;
@Component
public class MdcLoggingFilter implements Filter {
private static final String REQUEST_ID_HEADER = "X-Request-ID";
private static final String MDC_REQUEST_ID_KEY = "requestId";
// Ajoutez d'autres clés MDC si nécessaire (ex: userId, clientIp)
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Essayer de récupérer un ID existant (si propagé par un client/proxy) ou en générer un nouveau
String requestId = httpRequest.getHeader(REQUEST_ID_HEADER);
if (requestId == null || requestId.isEmpty()) {
requestId = UUID.randomUUID().toString();
}
// Placer l'ID dans le MDC
MDC.put(MDC_REQUEST_ID_KEY, requestId);
// Placer d'autres infos (ex: MDC.put("clientIp", request.getRemoteAddr());)
try {
// Continuer la chaîne de filtres et le traitement de la requête
chain.doFilter(request, response);
} finally {
// **Très important : Nettoyer le MDC après le traitement**
MDC.remove(MDC_REQUEST_ID_KEY);
// MDC.remove("clientIp");
// Alternative plus sûre si on ajoute beaucoup d'éléments: MDC.clear();
}
}
// init() et destroy() peuvent être laissés vides si non nécessaires
}
Ce filtre, une fois déclaré comme bean @Component, sera automatiquement appliqué par Spring Boot. Il génère un ID unique (ou réutilise un ID entrant via un en-tête) et le place dans le MDC sous la clé `requestId` avant de passer la main. Le bloc `finally` garantit que la clé est retirée du MDC, même en cas d'exception pendant le traitement.
L'équivalent avec WebFlux utiliserait l'interface `WebFilter` et travaillerait avec `ServerWebExchange`, mais le principe de mettre et retirer les données du MDC (souvent via le `Context` de Reactor qui gère la propagation du MDC) reste le même.
Intégrer les données MDC dans le format des logs
Une fois les données placées dans le MDC, il faut indiquer au framework de logging de les inclure dans les messages. Cela se fait en modifiant le pattern de formatage.
Le spécificateur de conversion standard pour récupérer une valeur du MDC est %X{key} ou parfois %mdc{key}, où key est la clé que vous avez utilisée pour stocker la donnée (par exemple, `requestId`).
Configuration dans application.properties :
# Ajouter [%X{requestId}] au pattern console
logging.pattern.console=%d{HH:mm:ss.SSS} %clr(%-5level){blue} [%X{requestId}] [%15.15t] %clr(%-40.40logger{39}){cyan} : %m%n%wEx
# Ajouter [%X{requestId}] au pattern fichier
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{requestId}] [%thread] %logger{50} - %message%n%exception
Configuration dans logback-spring.xml :
%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{requestId}] [%thread] %logger{36} - %msg%n%ex
Avec cette configuration, chaque ligne de log générée pendant le traitement d'une requête inclura automatiquement l'ID de cette requête :
14:50:05.100 INFO [a7f3b1d4-e8c9-4f2a-b1d9-abcd1234efgh] [http-nio-8080-exec-3] c.e.w.MonController : Requête reçue pour l'utilisateur xyz
14:50:05.150 DEBUG [a7f3b1d4-e8c9-4f2a-b1d9-abcd1234efgh] [http-nio-8080-exec-3] c.e.s.TraitementService: Début du traitement métier
14:50:05.200 INFO [a7f3b1d4-e8c9-4f2a-b1d9-abcd1234efgh] [http-nio-8080-exec-3] c.e.w.MonController : Traitement terminé avec succèsIl devient alors trivial de filtrer ou rechercher tous les logs correspondant à l'ID `a7f3b1d4-e8c9-4f2a-b1d9-abcd1234efgh`.
MDC et traitement asynchrone
Le principal défi avec le MDC est qu'il est basé sur des `ThreadLocal`. Lorsqu'une tâche est déléguée à un autre thread (par exemple via `@Async`, un `ExecutorService`, Project Reactor, ou lors de la publication/consommation de messages Kafka/RabbitMQ), le contexte MDC du thread original n'est pas automatiquement propagé au nouveau thread.
Cela signifie que les logs générés par le thread asynchrone n'auront pas les informations MDC (comme le `requestId`). Pour résoudre ce problème, il faut explicitement propager le contexte MDC. Plusieurs solutions existent :
- Manuellement : Récupérer le contexte MDC (`MDC.getCopyOfContextMap()`) avant de soumettre la tâche asynchrone, et le restaurer (`MDC.setContextMap(...)`) au début de l'exécution de la tâche dans le nouveau thread (en n'oubliant pas de le nettoyer ensuite). C'est fastidieux et source d'erreurs.
- Décorateurs d'Executor : Spring fournit des décorateurs (comme `DelegatingSecurityContextAsyncTaskExecutor`) qui peuvent être adaptés ou utilisés pour propager le contexte MDC aux tâches soumises via `@Async`.
- Intégrations spécifiques : Des frameworks comme Project Reactor (via les `Hooks` ou le `Context`) ou les intégrations de messagerie Spring (Kafka, AMQP) offrent parfois des mécanismes pour propager automatiquement le MDC ou des informations similaires.
- Micrometer Tracing (anciennement Sleuth) : C'est souvent la solution la plus simple et la plus complète. Lorsque Micrometer Tracing est activé, il gère non seulement la création et la propagation des Trace/Span IDs (qui sont souvent mis dans le MDC automatiquement), mais il décore également les Executors, Schedulers, etc., pour assurer la propagation du contexte de traçage (et donc implicitement des informations mises dans le MDC par Sleuth/Micrometer) à travers les frontières de threads.
Si vous utilisez déjà Micrometer Tracing pour le suivi distribué, il prendra souvent en charge une grande partie de la complexité liée à la propagation du contexte pour les logs, en plaçant automatiquement les `traceId` et `spanId` dans le MDC.
En conclusion, le MDC est un outil simple mais puissant pour améliorer considérablement la traçabilité et l'observabilité de vos applications Spring Boot en enrichissant vos logs avec un contexte spécifique aux requêtes, facilitant ainsi le débogage et l'analyse dans des environnements concurrents.