CodeGym /Courses /JAVA 25 SELF /Logging in multithreaded and web applications

Logging in multithreaded and web applications

JAVA 25 SELF
Level 63 , Lesson 2
Available

1. Logging thread safety

In single-threaded programs, everything is straightforward: one thread writes logs, and nothing interferes with it. But in real applications — web services, microservices — dozens and hundreds of threads run at the same time. Imagine several people writing with pens in the same line of a notebook simultaneously — the result would be, to put it mildly, unreadable.

Thread safety is the guarantee that even if 100500 threads write logs at the same time, messages won’t get mixed up, merged, or lost.

How is this implemented in libraries?

Modern logging libraries (Log4j 2, Logback, java.util.logging) are designed to be thread-safe out of the box. This means:

  • Each thread can safely call logger methods.
  • The library uses synchronization and queues internally so messages don’t interfere with each other.
  • Even if multiple threads write to the same file concurrently, the logs won’t get mixed up.

IMPORTANT: The logger itself (for example, a Logger from SLF4J or Log4j) can be used as a static final field in any class — this will not cause threading issues.

Example

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

        Thread t1 = new Thread(task, "First");
        Thread t2 = new Thread(task, "Second");
        t1.start();
        t2.start();
    }
}

In the logs, you will see tidy messages from both threads — without jumble or overlap.

2. Logging context: MDC (Mapped Diagnostic Context)

Imagine your application handles hundreds of requests simultaneously, each in its own thread. Messages flash by in the logs, but it’s unclear which request they belong to. You want to see not just “what happened,” but with whom and in which request it happened.

MDC (Mapped Diagnostic Context) is a special mechanism that lets you “attach” additional information to logs associated with the current thread. All messages written by the thread automatically receive this extra data.

Example: logging a request identifier

In a web application, you can assign a unique ID (for example, a UUID) to each request. Using MDC, this ID will be automatically added to all logs written by the thread serving the request.

What it looks like in 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 = () -> {
            // Generate a unique request identifier
            String requestId = UUID.randomUUID().toString();
            MDC.put("requestId", requestId); // add to MDC

            logger.info("Processing request");
            doSomeWork();
            logger.info("Finished processing");

            MDC.clear(); // make sure to clear it after completion!
        };

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

    static void doSomeWork() {
        logger.debug("Doing some work...");
    }
}

Log format configuration (for example, logback.xml):

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

Result:

12:01:23 [Thread-1] INFO  MdcExample [requestId=ad8d...f3] - Processing request
12:01:23 [Thread-1] DEBUG MdcExample [requestId=ad8d...f3] - Doing some work...
12:01:23 [Thread-1] INFO  MdcExample [requestId=ad8d...f3] - Finished processing

Important!

  • MDC works only within a single thread. If you hand work off to another thread (for example, via a thread pool), you need to pass MDC values manually (or use special libraries that do this automatically).
  • Don’t forget to clear MDC! If you don’t clear it, data may “leak” into the next request on the same thread (for example, in a web server’s thread pool). Use MDC.clear() in a finally block.

3. Logging in web applications

A web application is not just a program that starts and runs. It’s a real pipeline: requests come in, get processed, responses go out. And all of this happens concurrently, by the hundreds. Here logging is not a luxury, but a necessity!

What to log in web applications?

  • HTTP requests and responses: method, URL, parameters, response status, processing time.
  • Errors and exceptions: all unexpected failures, stack trace.
  • Business events: registration, login, checkout, payment, etc.
  • Technical details: interactions with the database and external services, operation execution times.

The main rule: log in such a way that a month later, when something breaks at 3 a.m., you can figure out what went wrong.

Example: logging an HTTP request (Spring Boot)

The simplest approach is to use a filter or aspect that logs every incoming request.

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

        long start = System.currentTimeMillis();
        try {
            chain.doFilter(request, response); // forward down the chain (to the controller)
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Response sent, processing time: {} ms", duration);
            MDC.clear();
        }
    }
}

Logging errors and exceptions

In web frameworks (for example, Spring) it’s common to use dedicated error handlers (@ExceptionHandler) to nicely log all unexpected failures.

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("An error occurred: ", ex); // log with the full stack trace!
        return "error"; // return an error page
    }
}

Integration with web frameworks

Almost all modern web frameworks (Spring, Jakarta EE, Micronaut, etc.) integrate with loggers out of the box. Usually, it’s enough to add the SLF4J/Logback dependency to the project — and all standard messages (application startup, request handling, errors) will be logged automatically.

4. Practice: example of logging in a multithreaded task

Let’s add multithreaded processing to our sample application (for example, an order processing service) and see how logging helps keep your head on straight.

Example: processing orders in multiple threads with 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("Starting order processing {}", orderId);
                    processOrder(orderId);
                    logger.info("Order {} processed successfully", orderId);
                } catch (Exception ex) {
                    logger.error("Error while processing order " + orderId, ex);
                } finally {
                    MDC.clear();
                }
            });
        }
        executor.shutdown();
    }

    static void processOrder(int orderId) throws InterruptedException {
        if (orderId % 2 == 0) {
            throw new RuntimeException("Simulated error for an even order");
        }
        Thread.sleep(500); // simulate work
    }
}

What’s happening:

  • Each order is processed in a separate thread.
  • A unique requestId is created for each thread (via MDC).
  • You can find all logs for a single order by this identifier.
  • Errors are logged with the full stack.

The log format is configured to display the requestId.

5. Important nuances and considerations

  • Threads, pools, and MDC. If you work with thread pools (and you likely do), remember: threads in a pool are reused! If you forget to clear MDC, data from one request can end up in another request’s logs. Always call MDC.clear() at the end of work.
  • MDC and asynchronous tasks. In asynchronous web frameworks (for example, Spring WebFlux), MDC doesn’t always work out of the box because request handling can hop between threads. For such cases, there are special extensions or adapters.
  • Logging in microservices. In a microservices architecture, it’s common to log not only a local request identifier but also a global one (traceId) that is propagated between services. This lets you trace the path of a request through the entire system (distributed tracing). Systems like Zipkin, Jaeger, and OpenTelemetry are often used for this.

6. Demonstration: difference between System.out.println and logging

System.out.println simply prints a line to the console. In a multithreaded environment:

  • Messages can be interleaved.
  • No information about time, thread, level, or context.
  • You can’t configure output to a file, format, or level-based filtering.

A logger writes structured messages, respects threads and levels, supports formatting, and can output to different destinations (file, console, network).

Comparison example

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

Conclusion:

  • System.out — messages can be interleaved, without time and level.
  • A logger — each message contains time, thread, level; you can filter and quickly find what you need.

7. Common mistakes when logging in multithreaded and web applications

Mistake #1: Using System.out.println instead of a logger. In a multithreaded environment, this leads to a “mess” in the console, no ability to filter messages, and loss of context information.

Mistake #2: Ignoring MDC or using it incorrectly. If you don’t use MDC to propagate a request/user identifier, the logs become meaningless — it’s impossible to understand which request an error belongs to. If you forget to clear MDC, data can “leak” into another request.

Mistake #3: Creating a logger as a local variable. It’s better to use a private static final Logger — the logger is created once per class, memory isn’t wasted, and you avoid risks of errors.

Mistake #4: Logging sensitive data. Passwords, credit card numbers, and personal data must not end up in logs — this is a security violation!

Mistake #5: Logging only “INFO” or only “ERROR.” Use appropriate levels: DEBUG for troubleshooting, INFO for business events, ERROR for failures. Don’t write everything at one level — otherwise logs lose their meaning.

Mistake #6: Not logging exception stack traces. If you write just logger.error("Error: " + ex.getMessage()), you lose information about the cause. Always log the full exception: logger.error("Error", ex).

Mistake #7: Homegrown loggers that aren’t thread-safe. If someone decides to “build their own logger” without synchronization — in a multithreaded environment it almost guarantees loss or corruption of logs.

1
Task
JAVA 25 SELF, level 63, lesson 2
Locked
Game Server
Game Server
1
Task
JAVA 25 SELF, level 63, lesson 2
Locked
Support Service
Support Service
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION