CodeGym /Kurse /JAVA 25 SELF /Logging in mehrfädigen und Web-Anwendungen

Logging in mehrfädigen und Web-Anwendungen

JAVA 25 SELF
Level 63 , Lektion 2
Verfügbar

1. Thread-Sicherheit beim Logging

In Single-Thread-Programmen ist alles einfach: Ein Thread schreibt Logs, niemand stört ihn. In realen Anwendungen — Web-Services, Microservices — laufen jedoch gleichzeitig Dutzende und Hunderte Threads. Stellen Sie sich vor, mehrere Personen würden gleichzeitig mit einem Stift in dieselbe Zeile eines Notizbuchs schreiben — das Ergebnis wäre gelinde gesagt unleserlich.

Thread-Sicherheit (thread safety) ist die Garantie, dass selbst wenn 100500 Threads gleichzeitig Logs schreiben, die Meldungen nicht durcheinandergeraten, nicht zusammenfließen und nicht verloren gehen.

Wie ist das in Bibliotheken umgesetzt?

Moderne Logging-Bibliotheken (Log4j 2, Logback, java.util.logging) sind von Anfang an so konzipiert, dass sie threadsicher sind. Das bedeutet:

  • Jeder Thread kann die Methoden des Loggers sicher aufrufen.
  • In der Bibliothek werden Synchronisation und Queues verwendet, damit sich Meldungen nicht gegenseitig stören.
  • Selbst wenn mehrere Threads gleichzeitig in dieselbe Datei schreiben, werden die Logs nicht vermischt.

WICHTIG: Den Logger selbst (z. B. ein Logger-Objekt aus SLF4J oder Log4j) kann man als static final Feld in jeder Klasse verwenden — das führt nicht zu Problemen mit Threads.

Beispiel

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("Thread {} schreibt Nachricht {}", Thread.currentThread().getName(), i);
            }
        };

        Thread t1 = new Thread(task, "Erster");
        Thread t2 = new Thread(task, "Zweiter");
        t1.start();
        t2.start();
    }
}

In den Logs sehen Sie saubere Meldungen von beiden Threads — ohne Kuddelmuddel und Überlagerungen.

2. Logging-Kontext: MDC (Mapped Diagnostic Context)

Stellen Sie sich vor: Ihre Anwendung verarbeitet gleichzeitig Hunderte Anfragen, jede in ihrem eigenen Thread. In den Logs blitzen Meldungen auf, aber es ist unklar, was zu welcher Anfrage gehört. Man möchte nicht nur „was passiert ist“ sehen, sondern auch, bei wem und in welcher Anfrage es passiert ist.

MDC (Mapped Diagnostic Context) ist ein spezieller Mechanismus, mit dem man zusätzliche Informationen an Logs „anhängen“ kann, die mit dem aktuellen Thread verknüpft sind. Alle Meldungen, die der Thread schreibt, erhalten diese Zusatzdaten automatisch.

Beispiel: Anfragen-ID loggen

In einer Web-Anwendung kann jeder Anfrage eine eindeutige ID zugewiesen werden (z. B. ein UUID). Mit MDC wird diese ID automatisch zu allen Logs hinzugefügt, die der Thread schreibt, der die Anfrage bedient.

So sieht es im Code aus (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 = () -> {
            // Wir erzeugen eine eindeutige Anfragen-ID
            String requestId = UUID.randomUUID().toString();
            MDC.put("requestId", requestId); // in MDC hinzufügen

            logger.info("Wir verarbeiten die Anfrage");
            doSomeWork();
            logger.info("Verarbeitung abgeschlossen");

            MDC.clear(); // unbedingt nach Abschluss leeren!
        };

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

    static void doSomeWork() {
        logger.debug("Wir führen die Arbeit aus...");
    }
}

Logformat konfigurieren (z. B. logback.xml):

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

Ergebnis:

12:01:23 [Thread-1] INFO  MdcExample [requestId=ad8d...f3] - Anfrage wird verarbeitet
12:01:23 [Thread-1] DEBUG MdcExample [requestId=ad8d...f3] - Arbeit wird ausgeführt...
12:01:23 [Thread-1] INFO  MdcExample [requestId=ad8d...f3] - Verarbeitung abgeschlossen

Wichtig!

  • MDC funktioniert nur innerhalb eines Threads. Wenn Sie die Arbeit an einen anderen Thread übergeben (z. B. über einen Thread-Pool), müssen Sie die MDC-Werte manuell übertragen (oder spezielle Bibliotheken verwenden, die das automatisch übernehmen).
  • Vergessen Sie nicht, MDC zu leeren! Wenn Sie es nicht leeren, können Daten in derselben Thread-Pool-Thread auf die nächste Anfrage „überlaufen“ (z. B. im Thread-Pool des Web-Servers). Verwenden Sie MDC.clear() im finally-Block.

3. Logging in Web-Anwendungen

Eine Web-Anwendung ist nicht einfach ein Programm, das gestartet wird und läuft. Es ist ein echtes Fließband: Anfragen kommen an, werden verarbeitet, Antworten gehen raus. Und das alles gleichzeitig, zu Hunderten. Hier ist Logging keine Kür, sondern Pflicht!

Was sollte man in Web-Anwendungen loggen?

  • HTTP-Anfragen und -Antworten: Methode, URL, Parameter, Antwortstatus, Bearbeitungszeit.
  • Fehler und Exceptions: alle unerwarteten Ausfälle, Stacktrace.
  • Business-Ereignisse: Registrierung, Login, Bestellung, Zahlung usw.
  • Technische Details: Interaktion mit der Datenbank, externen Services, Ausführungszeiten.

Die Hauptregel: Loggen Sie so, dass Sie in einem Monat, wenn um 3 Uhr nachts etwas kaputtgeht, nachvollziehen können, was schiefgelaufen ist.

Beispiel: Logging einer HTTP-Anfrage (Spring Boot)

Der einfachste Weg ist, einen Filter oder Aspekt zu verwenden, der jede eingehende Anfrage loggt.

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("Anfrage: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());

        long start = System.currentTimeMillis();
        try {
            chain.doFilter(request, response); // weiter in der Kette (zum Controller)
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Antwort gesendet, Bearbeitungszeit: {} ms", duration);
            MDC.clear();
        }
    }
}

Logging von Fehlern und Exceptions

In Web-Frameworks (z. B. Spring) verwendet man üblicherweise spezielle Fehlerhandler (@ExceptionHandler), um alle unerwarteten Ausfälle sauber zu loggen.

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("Ein Fehler ist aufgetreten: ", ex); // mit vollständigem Stacktrace loggen!
        return "error"; // Fehlerseite zurückgeben
    }
}

Integration mit Web-Frameworks

Fast alle modernen Web-Frameworks (Spring, Jakarta EE, Micronaut u. a.) integrieren sich „von Haus aus“ mit Loggern. In der Regel genügt es, die Abhängigkeit SLF4J/Logback ins Projekt aufzunehmen — und alle Standardmeldungen (Anwendungsstart, Anfrageverarbeitung, Fehler) werden automatisch geloggt.

4. Praxis: Beispiel für Logging in einer mehrfädigen Aufgabe

Fügen wir unserer Lernanwendung (z. B. einem Bestellverarbeitungsservice) eine mehrfädige Verarbeitung hinzu und schauen wir, wie Logging hilft, den Überblick zu behalten.

Beispiel: Auftragsverarbeitung in mehreren Threads mit 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("Wir beginnen mit der Verarbeitung von Auftrag {}", orderId);
                    processOrder(orderId);
                    logger.info("Auftrag {} wurde erfolgreich verarbeitet", orderId);
                } catch (Exception ex) {
                    logger.error("Fehler bei der Verarbeitung von Auftrag " + orderId, ex);
                } finally {
                    MDC.clear();
                }
            });
        }
        executor.shutdown();
    }

    static void processOrder(int orderId) throws InterruptedException {
        if (orderId % 2 == 0) {
            throw new RuntimeException("Fehlersimulation für einen geraden Auftrag");
        }
        Thread.sleep(500); // Arbeitssimulation
    }
}

Was passiert:

  • Jeder Auftrag wird in einem eigenen Thread verarbeitet.
  • Für jeden Thread wird eine eindeutige requestId erzeugt (über MDC).
  • Alle Logs zu einem Auftrag lassen sich über diese Kennung finden.
  • Fehler werden mit vollständigem Stacktrace geloggt.

Das Logformat ist so konfiguriert, dass die requestId angezeigt wird.

5. Wichtige Feinheiten und Besonderheiten

  • Threads, Pools und MDC. Wenn Sie mit Thread-Pools arbeiten (und das tun Sie sehr wahrscheinlich), merken Sie sich: Threads im Pool werden wiederverwendet! Wenn Sie MDC nicht leeren, können Daten von einer Anfrage in die Logs einer anderen gelangen. Rufen Sie am Ende immer MDC.clear() auf.
  • MDC und asynchrone Aufgaben. In asynchronen Web-Frameworks (z. B. Spring WebFlux) funktioniert MDC nicht immer „out of the box“, weil die Anfrageverarbeitung zwischen Threads springen kann. Für solche Fälle gibt es spezielle Erweiterungen oder Adapter.
  • Logging in Microservices. In einer Microservice-Architektur loggt man üblicherweise nicht nur die lokale Anfragenkennung, sondern auch eine globale (traceId), die zwischen Services weitergegeben wird. Das ermöglicht, den Weg der Anfrage durch das gesamte System zu verfolgen (Distributed Tracing). Dafür werden oft Systeme wie Zipkin, Jaeger, OpenTelemetry verwendet.

6. Demo: Unterschied zwischen System.out.println und Logging

System.out.println gibt einfach eine Zeichenkette in der Konsole aus. In einer mehrfädigen Umgebung:

  • Meldungen können sich vermischen.
  • Es gibt keine Informationen über Zeit, Thread, Level, Kontext.
  • Ausgabe in Datei, Format oder Filterung nach Level lässt sich nicht konfigurieren.

Logger — schreibt strukturierte Meldungen, berücksichtigt Threads, Level und Format und unterstützt Ausgaben an verschiedene Ziele (Datei, Konsole, Netzwerk).

Vergleichsbeispiel

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() + " Schritt " + i);
                logger.info("Logger: Schritt {}", i);
            }
        };
        new Thread(task, "T1").start();
        new Thread(task, "T2").start();
    }
}

Ergebnis:

  • System.out — Meldungen können durcheinandergehen, ohne Zeit und Level.
  • Logger — jede Meldung enthält Zeit, Thread und Level; man kann filtern und schnell das Gesuchte finden.

7. Typische Fehler beim Logging in mehrfädigen und Web-Anwendungen

Fehler Nr. 1: Verwendung von System.out.println statt Logger. In einer mehrfädigen Umgebung führt das zu einem Durcheinander in der Konsole, zur Unmöglichkeit, Meldungen zu filtern, und zum Verlust von Kontextinformationen.

Fehler Nr. 2: Ignorieren von MDC oder falsche Verwendung. Wenn man MDC nicht zur Übertragung der Anfragen-/Benutzerkennung nutzt, werden Logs sinnlos — es ist unmöglich zu verstehen, zu welcher Anfrage ein Fehler gehört. Wenn man vergisst, MDC zu leeren, können Daten in eine andere Anfrage „auslaufen“.

Fehler Nr. 3: Logger wird als lokale Variable erstellt. Verwenden Sie lieber einen private static final Logger — so wird der Logger einmal pro Klasse erstellt, Speicher wird nicht verschwendet und Fehler werden vermieden.

Fehler Nr. 4: Logging sensibler Daten. Passwörter, Kreditkartendaten und personenbezogene Daten dürfen nicht in Logs gelangen — das ist ein Sicherheitsverstoß!

Fehler Nr. 5: Es wird nur „INFO“ oder nur „ERROR“ geloggt. Verwenden Sie passende Level: DEBUG für Debugging, INFO für Business-Ereignisse, ERROR für Fehler. Schreiben Sie nicht alles mit demselben Level — sonst verlieren Logs ihren Sinn.

Fehler Nr. 6: Stacktraces von Exceptions werden nicht geloggt. Wenn man nur logger.error("Fehler: " + ex.getMessage()) schreibt, gehen Informationen über die Ursache verloren. Loggen Sie immer die komplette Exception: logger.error("Fehler", ex).

Fehler Nr. 7: Nicht threadsichere Eigenbau-Logger. Wenn jemand „seinen eigenen Logger“ ohne Synchronisation baut — ist in einer mehrfädigen Umgebung Datenverlust oder Log-Beschädigung nahezu garantiert.

1
Aufgabe
JAVA 25 SELF, Level 63, Lektion 2
Gesperrt
Spielserver
Spielserver
1
Aufgabe
JAVA 25 SELF, Level 63, Lektion 2
Gesperrt
Kundensupport
Kundensupport
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION