CodeGym /Cours /JAVA 25 SELF /Journalisation dans les applications multithreadées et we...

Journalisation dans les applications multithreadées et web

JAVA 25 SELF
Niveau 63 , Leçon 2
Disponible

1. Sécurité des threads pour la journalisation

Dans les programmes monothread, tout est simple : un thread écrit les logs, personne ne le perturbe. Mais dans les applications réelles – services web, microservices – des dizaines et des centaines de threads tournent simultanément. Imaginez que plusieurs personnes écrivent en même temps au stylo sur la même ligne d’un carnet : le résultat serait, pour le dire gentiment, illisible.

La sécurité des threads (thread safety), c’est la garantie que même si 100500 threads écrivent des logs en même temps, les messages ne se mélangent pas, ne fusionnent pas et ne se perdent pas.

Comment est-ce implémenté dans les bibliothèques ?

Les bibliothèques modernes de journalisation (Log4j 2, Logback, java.util.logging) sont conçues dès le départ pour être thread-safe. Cela signifie :

  • Chaque thread peut appeler les méthodes du logger en toute sécurité.
  • À l’intérieur de la bibliothèque, la synchronisation et des files d’attente sont utilisées afin que les messages ne se gênent pas.
  • Même si plusieurs threads écrivent simultanément dans le même fichier, les logs ne se mélangent pas.

IMPORTANT : Le logger lui-même (par exemple, l’objet Logger de SLF4J ou Log4j) peut être utilisé comme champ static final dans n’importe quelle classe — cela ne posera aucun problème lié aux threads.

Exemple

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MultiThreadedLoggerExample {
    private static final Logger logger = LoggerFactory.getLogger(MultiThreadedLoggerExample.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                logger.info("Le thread {} écrit le message {}", Thread.currentThread().getName(), i);
            }
        };

        Thread t1 = new Thread(task, "Premier");
        Thread t2 = new Thread(task, "Deuxième");
        t1.start();
        t2.start();
    }
}

Dans les logs, vous verrez des messages propres provenant des deux threads — sans fouillis ni chevauchement.

2. Contexte de journalisation : MDC (Mapped Diagnostic Context)

Imaginez : votre application traite des centaines de requêtes simultanément, chacune dans son propre thread. Les messages défilent dans les logs, mais il est difficile de savoir à quelle requête ils se rapportent. On veut voir non seulement « ce qui s’est passé », mais aussi avec qui et dans le cadre de quelle requête cela s’est produit.

MDC (Mapped Diagnostic Context) est un mécanisme spécial qui permet « d’attacher » aux logs des informations supplémentaires liées au thread en cours. Tous les messages écrits par ce thread reçoivent automatiquement ces données supplémentaires.

Exemple : journaliser l’identifiant de requête

Dans une application web, chaque requête peut recevoir un ID unique (par exemple, un UUID). Avec MDC, cet ID sera automatiquement ajouté à tous les logs écrits par le thread qui traite la requête.

À quoi cela ressemble dans le code (SLF4J + Logback) :

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;

public class MdcExample {
    private static final Logger logger = LoggerFactory.getLogger(MdcExample.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            // Génère un identifiant de requête unique
            String requestId = UUID.randomUUID().toString();
            MDC.put("requestId", requestId); // ajout dans le MDC

            logger.info("Traitement de la requête");
            doSomeWork();
            logger.info("Traitement terminé");

            MDC.clear(); // à nettoyer impérativement après la fin !
        };

        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");
        t1.start();
        t2.start();
    }

    static void doSomeWork() {
        logger.debug("Exécution en cours...");
    }
}

Configuration du format de log (par exemple, logback.xml) :

<encoder>
    <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n</pattern>
</encoder>

Résultat :

12:01:23 [Thread-1] INFO  MdcExample [requestId=ad8d...f3] - Traitement de la requête
12:01:23 [Thread-1] DEBUG MdcExample [requestId=ad8d...f3] - Exécution en cours...
12:01:23 [Thread-1] INFO  MdcExample [requestId=ad8d...f3] - Traitement terminé

Important !

  • MDC ne fonctionne que dans le cadre d’un seul thread. Si vous transférez le travail à un autre thread (par exemple, via un pool de threads), vous devez transmettre manuellement les valeurs du MDC (ou utiliser des bibliothèques spécialisées qui le font automatiquement).
  • N’oubliez pas de nettoyer le MDC ! Si vous ne le faites pas, les données peuvent « couler » vers la requête suivante dans le même thread (par exemple, dans un pool de threads du serveur web). Utilisez MDC.clear() dans un bloc finally.

3. Journalisation dans les applications web

Une application web, ce n’est pas juste un programme qui se lance et fonctionne. C’est une véritable chaîne de traitement : les requêtes arrivent, sont traitées, puis les réponses repartent. Et tout cela — simultanément, par centaines. Ici, la journalisation n’est pas un luxe, c’est une nécessité !

Que journaliser dans les applications web ?

  • Requêtes et réponses HTTP : méthode, URL, paramètres, statut de la réponse, temps de traitement.
  • Erreurs et exceptions : toutes les pannes inattendues, stack trace.
  • Événements métier : inscription, connexion, passage de commande, paiement, etc.
  • Détails techniques : interactions avec la base de données, services externes, temps d’exécution des opérations.

Règle principale : journalisez de sorte que, dans un mois, quand quelque chose casse à 3 heures du matin, vous puissiez comprendre ce qui s’est mal passé.

Exemple : journalisation d’une requête HTTP (Spring Boot)

Le moyen le plus simple est d’utiliser un filtre ou un aspect qui journalise chaque requête entrante.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Component
public class RequestLoggingFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        logger.info("Requête: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());

        long start = System.currentTimeMillis();
        try {
            chain.doFilter(request, response); // on passe dans la chaîne (vers le contrôleur)
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Réponse envoyée, durée de traitement: {} ms", duration);
            MDC.clear();
        }
    }
}

Journalisation des erreurs et exceptions

Dans les frameworks web (par exemple, Spring), il est d’usage d’utiliser des gestionnaires d’erreurs dédiés (@ExceptionHandler) pour journaliser proprement toutes les pannes inattendues.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public String handleException(Exception ex) {
        logger.error("Une erreur s'est produite: ", ex); // on journalise avec la stack trace complète !
        return "error"; // retourne la page d'erreur
    }
}

Intégration avec les frameworks web

Presque tous les frameworks web modernes (Spring, Jakarta EE, Micronaut, etc.) s’intègrent aux loggers « out of the box ». Il suffit généralement d’ajouter la dépendance SLF4J/Logback au projet — et tous les messages standard (démarrage de l’application, traitement des requêtes, erreurs) seront journalisés automatiquement.

4. Pratique : exemple de journalisation dans une tâche multithread

Ajoutons un traitement multithread à notre application d’apprentissage (par exemple, un service de traitement des commandes) et voyons comment la journalisation aide à garder la tête froide.

Exemple : traitement des commandes dans plusieurs threads avec MDC

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class OrderProcessingApp {
    private static final Logger logger = LoggerFactory.getLogger(OrderProcessingApp.class);

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int orderId = i;
            executor.submit(() -> {
                String requestId = UUID.randomUUID().toString();
                MDC.put("requestId", requestId);

                try {
                    logger.info("Début du traitement de la commande {}", orderId);
                    processOrder(orderId);
                    logger.info("Commande {} traitée avec succès", orderId);
                } catch (Exception ex) {
                    logger.error("Erreur lors du traitement de la commande " + orderId, ex);
                } finally {
                    MDC.clear();
                }
            });
        }
        executor.shutdown();
    }

    static void processOrder(int orderId) throws InterruptedException {
        if (orderId % 2 == 0) {
            throw new RuntimeException("Simulation d'erreur pour une commande paire");
        }
        Thread.sleep(500); // simulation du traitement
    }
}

Ce qui se passe :

  • Chaque commande est traitée dans un thread séparé.
  • Pour chaque thread, un requestId unique est créé (via le MDC).
  • Tous les logs d’une même commande peuvent être trouvés grâce à cet identifiant.
  • Les erreurs sont journalisées avec la pile complète.

Le format de log est configuré pour afficher le requestId.

5. Nuances et particularités importantes

  • Threads, pools et MDC. Si vous travaillez avec des pools de threads (et c’est certainement le cas), souvenez-vous : les threads d’un pool sont réutilisés ! Si vous oubliez de nettoyer le MDC, les données d’une requête peuvent se retrouver dans les logs d’une autre. Appelez toujours MDC.clear() en fin de traitement.
  • MDC et tâches asynchrones. Dans les frameworks web asynchrones (par exemple, Spring WebFlux), MDC ne fonctionne pas toujours « out of the box », car le traitement d’une requête peut passer d’un thread à l’autre. Pour ces cas, il existe des extensions ou des adaptateurs dédiés.
  • Journalisation dans les microservices. En architecture microservices, on journalise non seulement l’identifiant local de la requête, mais aussi un identifiant global (traceId) transmis entre services. Cela permet de suivre le parcours d’une requête à travers tout le système (distributed tracing). Pour cela, on utilise souvent des systèmes tels que Zipkin, Jaeger, OpenTelemetry.

6. Démonstration : différence entre System.out.println et la journalisation

System.out.println — se contente d’imprimer une chaîne dans la console. En environnement multithread :

  • Les messages peuvent se mélanger.
  • Pas d’information sur l’heure, le thread, le niveau, le contexte.
  • Impossible de configurer la sortie vers un fichier, le format, ou de filtrer par niveau.

Un logger écrit des messages structurés, tient compte des threads, des niveaux, du format, et prend en charge la sortie vers différents supports (fichier, console, réseau).

Exemple de comparaison

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintVsLogger {
    private static final Logger logger = LoggerFactory.getLogger(PrintVsLogger.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("System.out: " + Thread.currentThread().getName() + " étape " + i);
                logger.info("Logger: étape {}", i);
            }
        };
        new Thread(task, "T1").start();
        new Thread(task, "T2").start();
    }
}

Conclusion :

  • System.out — les messages peuvent arriver entremêlés, sans heure ni niveau.
  • Un logger — chaque message contient l’heure, le thread, le niveau, et il est possible de filtrer et de trouver rapidement ce dont vous avez besoin.

7. Erreurs typiques lors de la journalisation dans les applications multithreadées et web

Erreur n° 1 : utiliser System.out.println au lieu d’un logger. En environnement multithread, cela conduit à un « fouillis » dans la console, à l’impossibilité de filtrer les messages et à la perte d’informations contextuelles.

Erreur n° 2 : ignorer le MDC ou mal l’utiliser. Si vous n’utilisez pas MDC pour transmettre l’identifiant de la requête/de l’utilisateur, les logs deviennent vides de sens — impossible de comprendre à quelle requête l’erreur se rapporte. Si vous oubliez de nettoyer le MDC, les données peuvent « fuir » vers une autre requête.

Erreur n° 3 : créer le logger comme variable locale. Il est préférable d’utiliser un private static final Logger — ainsi le logger est créé une seule fois par classe, on n’utilise pas inutilement de mémoire et on évite des erreurs.

Erreur n° 4 : journaliser des données sensibles. Les mots de passe, numéros de cartes bancaires, données personnelles ne doivent pas se retrouver dans les logs — c’est une faille de sécurité !

Erreur n° 5 : ne journaliser qu’en « INFO » ou qu’en « ERROR ». Utilisez des niveaux adaptés : DEBUG pour le débogage, INFO pour les événements métier, ERROR pour les erreurs. Évitez d’écrire tout au même niveau — sinon les logs perdent leur sens.

Erreur n° 6 : ne pas journaliser les stack traces des exceptions. Si vous écrivez simplement logger.error("Erreur: " + ex.getMessage()), l’information sur la cause de l’erreur est perdue. Journalisez toujours l’exception complète : logger.error("Erreur", ex).

Erreur n° 7 : loggers maison non thread-safe. Si quelqu’un décide de « faire son propre logger » sans synchronisation — en environnement multithread, cela mène quasiment à coup sûr à une perte ou une corruption des logs.

1
Mission
JAVA 25 SELF, niveau 63, leçon 2
Bloqué
Serveur de jeu
Serveur de jeu
1
Mission
JAVA 25 SELF, niveau 63, leçon 2
Bloqué
Service d'assistance
Service d'assistance
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION