Як правильно логовати винятки?
Використовуйте логер замість printStackTrace():
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);
Коли НЕ використовувати логування винятків
- Очікувані бізнес-сценарії —
UserNotFoundExceptionпри логіні: це не помилка системи, а нормальний сценарій. Використовуйтеlog.warn()абоlog.info()замістьlog.error(). Якщо кожен невдалий логін пише ERROR — ваші алерти знеціняться (alert fatigue) - Retry-механізми — не логуйте кожну спробу retry на рівні ERROR, використовуйте
log.debug(). Інакше 5 спроб retry = 5 ERROR-логів, хоча система ще бореться - Batch-обробка — при 1000 елементах з 500 помилками використовуйте log sampling (не більше 10 однакових стеків на хвилину)
- Health check endpoint — помилки health check логуйте на DEBUG, не засміюйте ERROR-логи
- Circuit breaker open — коли CB відкритий, логуйте один раз при відкритті, не кожну відхилену просьбу
- Дубльоване логування — якщо виняток буде залогований на верхньому рівні (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. Що таке загортання винятків]]