Как правильно логировать исключения
Правильное логирование исключений строится на трёх базовых принципах:
🟢 Junior Level
Краткий ответ для собеседования (30 секунд)
Правильное логирование исключений строится на трёх базовых принципах:
- Использовать фасад логирования (SLF4J) вместо
e.printStackTrace()илиSystem.err. - Передавать исключение последним аргументом (
log.error("Failed to process orderId={}", orderId, e);). В SLF4J для исключения не нужно ставить заполнитель{}— библиотека автоматически извлечет и отформатирует полный стек-трейс. - Соблюдать золотое правило “Log OR Throw, never both”: либо перехватить и залогировать исключение (если вы его обрабатываете), либо обернуть и пробросить дальше (если обрабатывать должен вызывающий слой). Одновременный лог и проброс дублирует стек-трейс на каждом слое архитектуры.
Канонический пример на Java 21 со SLF4J
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderProcessingService {
private static final Logger log = LoggerFactory.getLogger(OrderProcessingService.class);
public void processPayment(String orderId, double amount) {
try {
executeGatewayCharge(orderId, amount);
} catch (PaymentGatewayException e) {
// Исключение e передается ПОСЛЕДНИМ аргументом БЕЗ фигурных скобок {}
log.error("Failed to charge credit card for orderId={}, amount={}", orderId, amount, e);
throw new OrderBusinessException("Payment failed", e); // Либо логируем, либо бросаем!
}
}
private void executeGatewayCharge(String id, double amount) throws PaymentGatewayException {
// Логика взаимодействия
}
}
Градация уровней логирования для исключений
| Уровень | Когда применять для исключений | Пример ситуации |
|---|---|---|
ERROR |
Неожиданные системные сбои, угроза доступности, инфраструктурные падения. Требует алерта дежурному инженеру. | DatabaseConnectionException, NullPointerException, падение брокера Kafka |
WARN |
Ожидаемые деградации системы, временные повторы (retry), срабатывание fallback-механизмов, исчерпание лимитов. | Ошибка вызова внешнего API при наличии Circuit Breaker / Retry |
INFO / DEBUG |
Ожидаемые клиентские и бизнес-ошибки, не являющиеся багом системы. | Неверный пароль при аутентификации, 404 Not Found, отмена операции клиентом |
🟡 Middle Level
Тонкости SLF4J: ловушка заполнителя {}
Одна из самых частых ошибок на собеседованиях:
// ❌ ГРУБАЯ ОШИБКА: стек-трейс НЕ будет залогирован!
// SLF4J подставит e в {} через toString() -> "Error: java.lang.NullPointerException: null"
log.error("Error occurred: {}", e);
// ❌ ПЛОХО: исключение превращено в строку вручную, стек потерян
log.error("Error: " + e.getMessage());
// ✅ ПРАВИЛЬНО: SLF4J проверяет последний аргумент, видит Throwable и печатает стек
log.error("Error occurred", e);
// ✅ ПРАВИЛЬНО с параметрами: для e фигурные скобки НЕ НУЖНЫ
log.error("Error processing orderId={} for userId={}", orderId, userId, e);
Антипаттерн “Log and Throw” (Залогируй и пробрось)
Если каждый разработчик в команде логирует исключение в блоке catch, а затем пробрасывает его выше, лог-файл засоряется лавиной одинаковых ошибок:
// ❌ АНТИПАТТЕРН: один инцидент будет залогирован 3 РАЗА!
// Слой DAO:
catch (SQLException e) {
log.error("DAO error", e);
throw new RepositoryException(e);
}
// Слой Service:
catch (RepositoryException e) {
log.error("Service error", e);
throw new ServiceException(e);
}
// Слой Controller:
catch (ServiceException e) {
log.error("Controller error", e); // Итого: 3 огромных одинаковых стектрейса в ELK!
}
// ✅ ИДИОМАТИЧНОЕ ПРАВИЛО:
// В нижних слоях: только оборачиваем и пробрасываем (throw new ServiceException(e))
// В верхнем слое (например, @RestControllerAdvice в Spring): логируем ОДИН РАЗ
MDC (Mapped Diagnostic Context) и распределенная трассировка
В распределенных микросервисах стек-трейс бесполезен без контекста запроса. Механизм org.slf4j.MDC позволяет сохранять метаданные в контексте текущего потока:
import org.slf4j.MDC;
public class RequestInterceptor {
public void handleRequest(String traceId, String userId, Runnable action) {
MDC.put("traceId", traceId);
MDC.put("userId", userId);
try {
action.run();
} finally {
// КРИТИЧЕСКИ ВАЖНО: очищать MDC при возврате потока в пул!
MDC.clear();
}
}
}
Все логи (включая сообщения об исключениях), вызванные внутри action.run(), будут автоматически обогащены полями traceId и userId в Logback / Log4j2.
Структурированное логирование (JSON Layout)
Для поиска в Elasticsearch / Kibana / Grafana Loki логи форматируются в структурированный JSON через logstash-logback-encoder:
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
Это разделяет строку лога на индексируемые поля: exception_class, exception_message, stack_trace, предотвращая разбиение стектрейса на десятки отдельных строк.
🔴 Senior Level
Защита от Log Flooding и Log Sampling
При аварии инфраструктуры (например, отказ базы данных под нагрузкой 20 000 RPS) сервис рискует сгенерировать гигабайты логов за секунды, переполнив диск и вызвав отказ хоста (Disk Full Outage).
Решения для Highload:
- Duplicate Message Filter (Logback):
Ограничивает количество повторов идентичных стеков в единицу времени.
<filter class="ch.qos.logback.classic.filter.DuplicateMessageFilter"> <AllowedRepetitions>5</AllowedRepetitions> </filter> - Асинхронные аппендеры (AsyncAppender / LMAX Disruptor):
Вывод логов отделяется от рабочего потока через кольцевой буфер. Параметр
discardingThresholdуказывает сбрасывать логи уровняINFOиDEBUGпри заполнении очереди более чем на 80%, резервируя ресурсы подERROR.
Безопасность: Log Scrubbing и защита PII (Personally Identifiable Information)
Попадание в логи персональных данных (номера банковских карт, пароли, паспортные данные, JWT-токены) влечет уголовную ответственность и штрафы регуляторов (PCI DSS, GDPR).
// ❌ КРИТИЧЕСКАЯ УЯЗВИМОСТЬ: утечка сырого токена в лог
log.error("Auth failed for token: {}", rawJwtToken, e);
// ✅ ПРАВИЛЬНО: логировать только идентификаторы или хеши
log.error("Auth failed for principalId: {}", principalId, e);
В конфигурации логгера настраиваются паттерны маскирования (MaskingConverter), автоматически заменяющие шаблоны кредитных карт (\b(?:\d{4}[ -]?){3}\d{4}\b) на ****-****-****-****.
Virtual Threads (Java 21) и контекст логирования
С появлением виртуальных потоков (Project Loom) традиционный ThreadLocal-контекст (на котором базируется MDC) сталкивается с проблемами:
- Создание миллионов виртуальных потоков с тяжелыми
ThreadLocal-картами создает нагрузку на сборщик мусора. - В Java 21 представлен механизм Scoped Values (JEP 429/446): легковесные, иммутабельные значения, привязанные к скоупу выполнения, которые заменяют
ThreadLocalи безопасны для параллельных задач внутри виртуальных потоков.
🎯 Шпаргалка для интервью
Вопросы для быстрой проверки
- Что произойдет, если написать
log.error("Failed: {}", e)вместоlog.error("Failed", e)? Ответ: В первом случае объект исключения подставится в фигурные скобки через методe.toString(), и стек-трейс не будет напечатан. Во втором случае SLF4J распознаетThrowableкак последний аргумент и напечатает полный стек-трейс. - В чем заключается вред антипаттерна “Log and Throw”? Ответ: Исключение логируется на каждом слое архитектуры (репозиторий, сервис, контроллер). В результате в логах появляется несколько огромных дублирующихся стек-трейсов для одного и того же сбоя, что искажает статистику мониторинга и затрудняет расследование инцидентов.
- Почему в блоке
finallyобязательно вызыватьMDC.clear()? Ответ: Потоки веб-серверов (Tomcat, Jetty) переиспользуются из пула потоков. Если не очистить MDC, контекст предыдущего пользователя (userId,traceId) «прилипнет» к следующему запросу, выполняемому на этом же потоке. - На каком уровне логирования следует фиксировать ошибку неверного пароля пользователя?
Ответ: На уровне
WARNилиINFO. Это нормальный ожидаемый сценарий взаимодействия с клиентом. Логирование таких ошибок на уровнеERRORприведет к ложным срабатываниям дежурных алертов (Alert Fatigue).
Типичные ошибки и Red Flags
- ❌ Использование
e.printStackTrace()илиSystem.out.println()вместо логгера: Признак отсутствия опыта в production-системах. - ❌ Логирование паролей, CVV кодов и личных данных (PII) в сообщениях ошибок: Грубое нарушение требований безопасности.
- ❌ Практика “Log and Throw” (одновременное логирование и проброс): Засорение логов каскадными дублями стектрейсов.
- ❌ Логирование
log.error(e.getMessage()): Потеря имени класса исключения и полного стек-трейса.