JavaRush /Курсы /JAVA 25 SELF /Логирование в многопоточных и web-приложениях

Логирование в многопоточных и web-приложениях

JAVA 25 SELF
63 уровень , 2 лекция
Открыта

1. Потокобезопасность логирования

В однопоточных программах всё просто: один поток пишет логи, никто ему не мешает. А вот в реальных приложениях — web-сервисах, микросервисах — одновременно крутятся десятки и сотни потоков. Представьте, если бы несколько человек одновременно писали ручкой в одну и ту же строчку блокнота — результат был бы, мягко говоря, нечитаемым.

Потокобезопасность (thread safety) — это гарантия, что даже если 100500 потоков одновременно пишут логи, сообщения не перепутаются, не сольются и не потеряются.

Как это реализовано в библиотеках?

Современные библиотеки логирования (Log4j 2, Logback, java.util.logging) изначально спроектированы так, чтобы быть потокобезопасными. Это означает:

  • Каждый поток может безопасно вызывать методы логгера.
  • Внутри библиотеки используются синхронизация и очереди, чтобы сообщения не мешали друг другу.
  • Даже если несколько потоков одновременно пишут в один и тот же файл, логи не перепутаются.

ВАЖНО: Сам логгер (например, объект Logger из SLF4J или Log4j) можно использовать как static final поле в любом классе — это не приведёт к проблемам с потоками.

Пример

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.currentThread().getName(), i);
            }
        };

        Thread t1 = new Thread(task, "Первый");
        Thread t2 = new Thread(task, "Второй");
        t1.start();
        t2.start();
    }
}

В логах вы увидите аккуратные сообщения от обоих потоков — без каши и наложения.

2. Контекст логирования: MDC (Mapped Diagnostic Context)

Представьте: ваше приложение обрабатывает сотни запросов одновременно, каждый — в своём потоке. В логах мелькают сообщения, но непонятно, что к какому запросу относится. Хочется видеть не просто «что случилось», а с кем и в каком запросе это случилось.

MDC (Mapped Diagnostic Context) — это специальный механизм, который позволяет «прикрепить» дополнительную информацию к логам, связанную с текущим потоком. Все сообщения, которые пишет поток, автоматически получают эти дополнительные данные.

Пример: логируем идентификатор запроса

В web-приложении каждому запросу можно назначить уникальный ID (например, UUID). С помощью MDC этот ID будет автоматически добавляться ко всем логам, которые пишет поток, обслуживающий запрос.

Как это выглядит в коде (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 = () -> {
            // Генерируем уникальный идентификатор запроса
            String requestId = UUID.randomUUID().toString();
            MDC.put("requestId", requestId); // добавляем в MDC

            logger.info("Обрабатываем запрос");
            doSomeWork();
            logger.info("Завершили обработку");

            MDC.clear(); // обязательно очищаем после завершения!
        };

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

    static void doSomeWork() {
        logger.debug("Выполняем работу...");
    }
}

Настройка формата лога (например, logback.xml):

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

Результат:

12:01:23 [Поток-1] INFO  MdcExample [requestId=ad8d...f3] - Обрабатываем запрос
12:01:23 [Поток-1] DEBUG MdcExample [requestId=ad8d...f3] - Выполняем работу...
12:01:23 [Поток-1] INFO  MdcExample [requestId=ad8d...f3] - Завершили обработку

Важно!

  • MDC работает только в рамках одного потока. Если вы передаёте работу в другой поток (например, через пул потоков), вам нужно вручную передать значения MDC (или использовать специальные библиотеки, которые делают это автоматически).
  • Не забывайте очищать MDC! Если не очистить, данные могут «перетечь» на следующий запрос в том же потоке (например, в пуле потоков web-сервера). Используйте MDC.clear() в finally-блоке.

3. Логирование в web-приложениях

Web-приложение — это не просто программа, которая запустилась и работает. Это настоящий конвейер: запросы прилетают, обрабатываются, уходят ответы. И всё это — одновременно, сотнями. Здесь логирование — не роскошь, а необходимость!

Что логировать в web-приложениях?

  • HTTP-запросы и ответы: метод, URL, параметры, статус ответа, время обработки.
  • Ошибки и исключения: все неожиданные сбои, stack trace.
  • Бизнес-события: регистрация, вход, оформление заказа, оплата и т.д.
  • Технические детали: взаимодействие с базой данных, внешними сервисами, время выполнения операций.

Главное правило: логируйте так, чтобы через месяц, когда что-то сломается в 3 часа ночи, вы смогли разобраться, что пошло не так.

Пример: логирование HTTP-запроса (Spring Boot)

Самый простой способ — использовать фильтр или аспект, который будет логировать каждый входящий запрос.

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("Запрос: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());

        long start = System.currentTimeMillis();
        try {
            chain.doFilter(request, response); // дальше по цепочке (к контроллеру)
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Ответ отправлен, время обработки: {} мс", duration);
            MDC.clear();
        }
    }
}

Логирование ошибок и исключений

В web-фреймворках (например, Spring) принято использовать специальные обработчики ошибок (@ExceptionHandler), чтобы красиво логировать все неожиданные сбои.

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("Произошла ошибка: ", ex); // логируем с полным stack trace!
        return "error"; // возвращаем страницу ошибки
    }
}

Интеграция с web-фреймворками

Почти все современные web-фреймворки (Spring, Jakarta EE, Micronaut и др.) интегрируются с логгерами «из коробки». Обычно достаточно добавить зависимость SLF4J/Logback в проект — и все стандартные сообщения (старта приложения, обработки запросов, ошибок) будут логироваться автоматически.

4. Практика: Пример логирования в многопоточной задаче

Давайте добавим в наше учебное приложение (например, сервис обработки заказов) многопоточную обработку и посмотрим, как логирование помогает не потерять голову.

Пример: обработка заказов в нескольких потоках с 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("Начинаем обработку заказа {}", orderId);
                    processOrder(orderId);
                    logger.info("Заказ {} обработан успешно", orderId);
                } catch (Exception ex) {
                    logger.error("Ошибка при обработке заказа " + orderId, ex);
                } finally {
                    MDC.clear();
                }
            });
        }
        executor.shutdown();
    }

    static void processOrder(int orderId) throws InterruptedException {
        if (orderId % 2 == 0) {
            throw new RuntimeException("Симуляция ошибки для чётного заказа");
        }
        Thread.sleep(500); // имитация работы
    }
}

Что происходит:

  • Каждый заказ обрабатывается в отдельном потоке.
  • Для каждого потока создаётся уникальный requestId (через MDC).
  • Все логи по одному заказу можно найти по этому идентификатору.
  • Ошибки логируются с полным стеком.

Формат лога настроен так, чтобы показывать requestId.

5. Важные нюансы и особенности

  • Потоки, пулы и MDC. Если работаете с пулами потоков (а вы наверняка работаете), запомните: потоки в пуле переиспользуются! Если забыть очистить MDC, данные от одного запроса могут попасть в логи другого. Всегда вызывайте MDC.clear() в конце работы.
  • MDC и асинхронные задачи. В асинхронных web-фреймворках (например, Spring WebFlux) MDC не всегда работает «из коробки», потому что обработка запроса может прыгать между потоками. Для таких случаев существуют специальные расширения или адаптеры.
  • Логирование в микросервисах. В микросервисной архитектуре принято логировать не только локальный идентификатор запроса, но и глобальный (traceId), который передаётся между сервисами. Это позволяет проследить путь запроса через всю систему (distributed tracing). Для этого часто используются системы вроде Zipkin, Jaeger, OpenTelemetry.

6. Демонстрация: разница между System.out.println и логированием

System.out.println — просто печатает строку в консоль. В многопоточной среде:

  • Сообщения могут перемешиваться.
  • Нет информации о времени, потоке, уровне, контексте.
  • Нельзя настроить вывод в файл, формат, фильтрацию по уровню.

Логгер — пишет структурированные сообщения, учитывает потоки, уровни, формат, поддерживает вывод в разные места (файл, консоль, сеть).

Пример сравнения

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

Вывод:

  • System.out — сообщения могут идти вперемешку, без времени и уровня.
  • Логгер — каждое сообщение содержит время, поток, уровень, можно отфильтровать и быстро найти нужное.

7. Типичные ошибки при логировании в многопоточных и web-приложениях

Ошибка №1: Использование System.out.println вместо логгера. В многопоточной среде это приводит к «каше» в консоли, невозможности фильтровать сообщения и потере информации о контексте.

Ошибка №2: Игнорирование MDC или неправильное его использование. Если не использовать MDC для передачи идентификатора запроса/пользователя, логи становятся бессмысленными — невозможно понять, к какому запросу относится ошибка. Если забыть очищать MDC, данные могут «утечь» в другой запрос.

Ошибка №3: Логгер создаётся как локальная переменная. Лучше использовать private static final Logger — так логгер создаётся один раз на класс, не тратится память, и нет риска ошибок.

Ошибка №4: Логирование чувствительных данных. В логи не должны попадать пароли, банковские карты, персональные данные — это нарушение безопасности!

Ошибка №5: Логирование только "INFO" или только "ERROR". Используйте подходящие уровни: DEBUG для отладки, INFO для бизнес-событий, ERROR для ошибок. Не стоит всё писать одним уровнем — иначе логи теряют смысл.

Ошибка №6: Не логируются stack trace исключений. Если писать просто logger.error("Ошибка: " + ex.getMessage()), теряется информация о причине ошибки. Всегда логируйте полное исключение: logger.error("Ошибка", ex).

Ошибка №7: Не потокобезопасные самописные логгеры. Если кто-то решил «сделать свой логгер» без синхронизации — в многопоточной среде это почти гарантированная потеря или порча логов.

1
Задача
JAVA 25 SELF, 63 уровень, 2 лекция
Недоступна
Игровой сервер
Игровой сервер
1
Задача
JAVA 25 SELF, 63 уровень, 2 лекция
Недоступна
Служба поддержки
Служба поддержки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ