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: Не потокобезопасные самописные логгеры. Если кто-то решил «сделать свой логгер» без синхронизации — в многопоточной среде это почти гарантированная потеря или порча логов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ