Питання 18 · Розділ 7

Як правильно логовати винятки?

Використовуйте логер замість printStackTrace():

Мовні версії: English Russian Ukrainian

Junior Level

Базове правило

Використовуйте логер замість printStackTrace():

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    // Logger — static final, один на клас
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void processOrder(Long orderId) {
        try {
            // business logic
        } catch (Exception e) {
            // Виняток — останнім аргументом, є контекст
            log.error("Failed to process order id={}", orderId, e);
        }
    }
}

Рівні логування

  • log.error() — критичні помилки, що потребують уваги
  • log.warn() — попередження, система працює але є ризик
  • log.info() — важливі події (запуск, завершення)
  • log.debug() — детальне налагодження

З параметрами

log.error("User id={} not found", userId, exception);
// exception НЕ потрапить в {}, він піде у стек-трейс

Як працює SLF4J: SLF4J замінює {} на значення аргументів зліва направо. Але є special case: якщо останній аргументThrowable, SLF4J не підставляє його в {}, а вилучає повний стек-трейс і додає його до повідомлення.

// {} замінюється на userId, exception — останнім аргументом → стек буде в лозі
log.error("User id={} not found", userId, exception);

// {} замінюється на e.getMessage(), exception НЕ останнім → стек НЕ потрапить до логу!
log.error("Error: {}", exception, userId);

// Немає {}, але exception останнім → стек все одно буде
log.error("Critical failure", exception);

Часта помилка: log.error("Error: {}", e) — тут e підставляється в {} як рядок (e.toString()), але стек-трейс НЕ виводиться. Завжди передавайте виняток останнім аргументом, окремо від {}.

Чого не робити

// ПОГАНО — немає контексту, неможливо зрозуміти де помилка
log.error(e.getMessage());

// ПОГАНО — e як параметр {}, не як виняток (стек не потрапить до логу)
log.error("Error: {}", e);

// ПОГАНО — логування та printStackTrace дублюють вивід
log.error("Error", e);
e.printStackTrace();

// ДОБРЕ — виняток останнім аргументом, є контекст
log.error("Failed to process user id={}", userId, e);

Коли НЕ використовувати логування винятків

  1. Очікувані бізнес-сценаріїUserNotFoundException при логіні: це не помилка системи, а нормальний сценарій. Використовуйте log.warn() або log.info() замість log.error(). Якщо кожен невдалий логін пише ERROR — ваші алерти знеціняться (alert fatigue)
  2. Retry-механізми — не логуйте кожну спробу retry на рівні ERROR, використовуйте log.debug(). Інакше 5 спроб retry = 5 ERROR-логів, хоча система ще бореться
  3. Batch-обробка — при 1000 елементах з 500 помилками використовуйте log sampling (не більше 10 однакових стеків на хвилину)
  4. Health check endpoint — помилки health check логуйте на DEBUG, не засміюйте ERROR-логи
  5. Circuit breaker open — коли CB відкритий, логуйте один раз при відкритті, не кожну відхилену просьбу
  6. Дубльоване логування — якщо виняток буде залогований на верхньому рівні (GlobalExceptionHandler), не логуйте його ще раз на кожному шарі. Один виняток — один ERROR-лог

Caveat: PII (Personally Identifiable Information) у логах

Ніколи не логуйте чутливі дані разом із винятками. Це включає:

  • Паролі, токени, CVV коди
  • Email, телефон, паспортні дані (GDPR!)
  • IP-адреси (в деяких юрисдикціях)
// ПОГАНО — пароль у лозі
log.error("Auth failed for user={} with password={}", username, password);

// ДОБРЕ — тільки контекст без чутливих даних
log.error("Auth failed for user={}", username);

Навіть якщо логер підтримує маскування, краще взагалі не передавати такі дані в логер. Налаштуйте лог-фреймворк на редактування (scrubbing) на рівні appender’а, а не в кожному виклику окремо.


Middle Level

SLF4J магія останнього аргументу

SLF4J шукає останній аргумент методу. Якщо це Throwable, він обробляється особливо — вилучається стек-трейс, навіть якщо у рядку немає {}:

log.error("Critical failure", exception); // Стек буде в лозі

MDC (Mapped Diagnostic Context)

У розподілених системах стек-трейс марний без контексту запиту:

MDC.put("traceId", traceId);
MDC.put("userId", userId);
try {
    orderService.process(orderId);
    // У лозі буде traceId та userId
} catch (Exception e) {
    log.error("Failed to process order id={}", orderId, e);
} finally {
    MDC.clear(); // Обов'язково — інакше traceId витече в наступний запит
}

JSON Layout

В ELK/Splunk логи мають бути JSON:

{
  "timestamp": "2024-01-15T10:30:00Z",
  "level": "ERROR",
  "message": "Failed to process order",
  "exception_class": "java.sql.SQLException",
  "stack_trace": "...",
  "traceId": "abc-123"
}

Переваги:

  • Індексування за exception_class
  • Пошук за stack_trace
  • Немає проблем з багаторядковими логами

Cause Analysis

Переконайтеся, що логер виводить всі рівні cause. Корінь зла може бути на 5-му рівні вкладеності.


Senior Level

Log Sampling

При величезному навантаженні та масових помилках можна «потонути» у логах (Log Flooding). Просунуті логери налаштовують ліміти:

<!-- Logback — не більше 10 однакових стеків на хвилину -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.DuplicateMessageFilter">
        <AllowedRepetitions>10</AllowedRepetitions>
    </filter>
</appender>

Async Appenders

Завжди використовуйте асинхронне логування — відокремлює запис на диск/мережу від бізнес-потоку:

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    <includeCallerData>false</includeCallerData> <!-- для прискорення -->
</appender>

Ризик: втрата останніх логів при падінні JVM.

Log Scrubbing (PII)

Ніколи не логуйте чутливі дані (паролі, CVV, токени). Сучасні логери підтримують маскування:

// Логер автоматично маскує
log.error("Auth failed for user with password={}", password);
// У лозі: password=****

ShortenedThrowableConverter

Не друкуйте 200 рядків стека Hibernate. Зазвичай перших 20 кадрів достатньо:

<conversionRule conversionWord="ex"
    converterClass="com.example.ShortenedThrowableConverter" />

Generic Catch-all

Логування catch (Exception e) на самому верхньому рівні обов’язкове, але має супроводжуватися алертингом (Prometheus/Grafana):

meterRegistry.counter("errors.unhandled").increment();
alertService.send("Unhandled exception: " + e.getMessage());

Діагностика

  • Logback Configuration — налаштуйте <shortenedThrowableConverter>
  • Cause Analysis — логер має виводити всі рівні cause
  • Structured Logging — JSON формат для ELK/Splunk
  • Metric-driven — кожен виняток через Micrometer в Prometheus

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Виняток — завжди останнім аргументом в log.error("msg {}", param, e); SLF4J вилучає стек-трейс з останнього аргументу
  • НЕ використовуйте log.error("Error: {}", e) — тут e підставляється в {} як рядок, стек НЕ виводиться
  • Використовуйте MDC (traceId, userId) для контексту в розподілених системах; обов’язково MDC.clear() в finally
  • Для highload: async appender, log sampling (не більше 10 однакових стеків/хв), shortenedThrowableConverter
  • Очікувані бізнес-сценарії логуйте на warn/info, не на error — інакше алерти знеціняться (alert fatigue)
  • Ніколи не логуйте PII: паролі, токени, CVV, email (GDPR!), IP-адреси

Часті уточнюючі запитання:

  • Як SLF4J обробляє виняток? — Якщо останній аргумент — Throwable, SLF4J не підставляє його в {}, а вилучає повний стек-трейс
  • Чому не дублювати логування? — Якщо GlobalExceptionHandler вже логує, не логуйте на кожному шарі — один виняток = один ERROR-лог
  • Що таке Log Sampling? — Обмеження кількості однакових повідомлень (наприклад, 10/хв) щоб не «потонути» у логах при масових помилках
  • Коли НЕ логувати виняток? — Очікувані бізнес-сценарії (UserNotFoundException при логіні), retry-спроби, health check, circuit breaker open

Червоні прапорці (НЕ говорити):

  • “Логую log.error(e.getMessage()) без контексту” — Неможливо зрозуміти де помилка і що її викликала
  • “Логую пароль разом з винятком” — Порушення безпеки (GDPR, PCI DSS)
  • “Логую і printStackTrace одночасно” — Дублювання, засміює логи
  • “Кожну retry на рівні ERROR” — 5 спроб = 5 ERROR-логів, алерти стають марними

Пов’язані теми:

  • [[17. Що робить метод printStackTrace()]]
  • [[16. Що таке stack trace]]
  • [[13. Чи можна створювати кастомні винятки]]
  • [[20. Чому не варто ковтати винятки (порожній catch)]]
  • [[19. Що таке загортання винятків]]