1. Потокобезпечність логування
В однопоточних програмах усе просто: один потік пише логи, ніхто йому не заважає. А от у реальних застосунках — веб‑сервісах, мікросервісах — одночасно працюють десятки й сотні потоків. Уявіть, якби кілька людей одночасно писали ручкою в один і той самий рядок блокнота — результат був би, м’яко кажучи, нечитабельним.
Потокобезпечність (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) — це спеціальний механізм, який дозволяє «прикріпити» додаткову інформацію до логів, пов’язану з поточним потоком. Усі повідомлення, які пише потік, автоматично отримують ці додаткові дані.
Приклад: логування ідентифікатора запиту
У веб‑застосунку кожному запиту можна призначити унікальний 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! Якщо не очистити, дані можуть «перетекти» на наступний запит у тому самому потоці (наприклад, у пулі потоків веб‑сервера). Використовуйте MDC.clear() у блоці finally.
3. Логування у веб-застосунках
Веб‑застосунок — це не просто програма, яка запустилася та працює. Це справжній конвеєр: запити прилітають, обробляються, йдуть відповіді. І все це — одночасно, сотнями. Тут логування — не розкіш, а необхідність!
Що логувати у веб-застосунках?
- 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();
}
}
}
Логування помилок і винятків
У веб‑фреймворках (наприклад, 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"; // повертаємо сторінку помилки
}
}
Інтеграція з веб-фреймворками
Майже всі сучасні веб‑фреймворки (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 та асинхронні завдання. В асинхронних веб‑фреймворках (наприклад, 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. Типові помилки під час логування в багатопотокових і веб-застосунках
Помилка № 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: Непотокобезпечні самописні логери. Якщо хтось вирішив «зробити свій логер» без синхронізації — у багатопоточному середовищі це майже гарантована втрата або псування логів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ