Опирается на правила:
R-ERR-LOG-1…R-ERR-LOG-4иR-ERR-LOG-X1…R-ERR-LOG-X2из Error Handling Style Guide → раздел 4. Логирование исключений.
Важно знать
DomainException— WARN в edge-handler'е. Это ожидаемая ошибка (бизнес-правило сработало), не баг сервиса.IntegrationException— WARN при одиночном сбое (CB ещё закрыт). ERROR когда CB открылся — это уже инцидент.TechnicalExceptionи catch-all (Throwable) — ERROR с полным stacktrace и структурным контекстом (request-id, customer-id, operation).- Логируем один раз — на edge. Не на каждом уровне call stack. Иначе одна ошибка превращается в 5 строк лога с разной полнотой контекста.
log.error("...", e); throw e;— двойное логирование запрещено. Либо логируй и обработай, либо проброс — выбирай.log.error(e.getMessage())без объектаe— теряется stacktrace. Используйlog.error("Context: {}", value, e)— последний аргумент без{}.
Логирование исключения — это не «на всякий случай», это источник правды для оператора и алёрта. Перелогирование (одна ошибка в трёх местах) создаёт шум; недолог (catch без логирования) скрывает проблему. Уровень — это семантика для алёртов: WARN = «нормально, в норме»; ERROR = «надо смотреть». Раскрытие правил R-ERR-LOG-* ниже.
DomainException — WARN
R-ERR-LOG-1: бизнес-правило сработало. Это ожидаемая ситуация, не баг.
@ExceptionHandler(DomainException.class)
public ResponseEntity<ProblemDetail> handleDomain(DomainException ex) {
log.warn("Domain rule violated: {}", ex.getMessage(), ex); // ← WARN
// ...
}
Почему не ERROR:
- Ложные алёрты. ERROR-алёрты обычно настроены «звонить on-call». Если каждый
InsufficientFundsExceptionшлёт SMS дежурному в 3 часа ночи, оператор быстро отключит алёрты. - DomainException — это нормально. Пользователь нажал «Списать», денег не хватило — это ожидаемая ветка флоу, а не сбой. Логирование нужно для аналитики (сколько раз сегодня сработало это правило), но не для алёрта «срочно лечить».
- Метрики разделяют. На WARN-исключения смотрим через
app_errors_total{type="domain"}. Резкий рост = бизнес-условие изменилось (не уведомили). Стабильный baseline = всё ок.
IntegrationException — WARN или ERROR
R-ERR-LOG-2: зависит от состояния CircuitBreaker'а.
WARN — одиночный сбой во внешней системе, CB ещё закрыт. Пара 5xx в потоке нормальной нагрузки — норма (сети, deploy внешки, etc.).
@ExceptionHandler(SberRegisterException.class)
public ResponseEntity<ProblemDetail> handleSberRegister(SberRegisterException ex) {
log.warn("Sber register failed: {}", ex.getMessage(), ex); // ← WARN
// ...
}
ERROR — CB открылся. Это уже инцидент: внешка деградировала достаточно, чтобы наш CB защитил основную нагрузку.
@ExceptionHandler(SberUnavailableException.class)
public ResponseEntity<ProblemDetail> handleSberUnavailable(SberUnavailableException ex) {
log.error("Sber circuit breaker open", ex); // ← ERROR
// ...
}
(SberUnavailableException бросается из fallback-метода после CallNotPermittedException. См. Где throw, где catch.)
На практике алёрт «CB открыт» лучше делать через метрику Resilience4j (resilience4j_circuitbreaker_state), а не через лог-агрегатор — метрика быстрее и надёжнее.
TechnicalException и catch-all — ERROR
R-ERR-LOG-3: внутренняя проблема — полный stacktrace и контекст.
@ExceptionHandler(TechnicalException.class)
public ResponseEntity<ProblemDetail> handleTechnical(TechnicalException ex) {
log.error("Technical error", ex); // ← ERROR + stacktrace
// ...
}
@ExceptionHandler(Throwable.class)
public ResponseEntity<ProblemDetail> handleUnexpected(Throwable ex) {
log.error("Unexpected error — not in hierarchy", ex); // ← ERROR + сигнал бага
// ...
}
Что важно при ERROR:
- Полный stacktrace. Передаём объект
exпоследним аргументом — SLF4J/Logback сам выведет stacktrace. - Структурный контекст через MDC.
requestId,userId,traceIdуже в MDC (см. Observability Style Guide → R-OBS-MDC-*) — Logback включает их в JSON-output. - Алёрт. ERROR-логи или
unexpected-counter — основа алёртов on-call.
Логируем один раз — на edge
R-ERR-LOG-4: исключение логируется в @RestControllerAdvice, и больше нигде.
// ПЛОХО — двойное логирование
@UseCaseHandler
public Order handle(CancelOrderCommand cmd) {
try {
// ...
} catch (DomainException e) {
log.warn("Domain error in cancel", e); // ← лог №1
throw e;
}
}
@ExceptionHandler(DomainException.class)
public ResponseEntity<...> handleDomain(DomainException ex) {
log.warn("Domain rule violated", ex); // ← лог №2 (одна и та же ошибка)
// ...
}
Что не так:
- Одна ошибка = два-три лога разного качества (где-то полный stacktrace, где-то только message). Оператор путается.
- Метрика
app_errors_totalинкрементируется один раз (в advice), но лог-stream показывает «несколько ошибок» — путаница. - Контекст разный. На каждом уровне call stack доступны разные данные. Делать «полный лог» нужно один раз — на edge, где собран максимальный контекст.
Принцип: исключение проходит насквозь до edge без логирования. Edge — единственное место, где логируется.
Что запрещено
R-ERR-LOG-X1: log.error("...", e); throw e; — двойное логирование.
// ПЛОХО
try {
paymentPort.register(cmd);
} catch (IntegrationException e) {
log.error("Payment failed", e); // ← лог №1
throw e; // ← дальше advice залогирует №2
}
Это самый частый антипаттерн. Кажется, что «полезно залогировать прямо тут на случай если выше не залогируют», но:
- Advice обязательно залогирует (catch-all
Throwableв crash-сценарии тоже залогирует). - Двойной лог попадает в Loki/ELK как два события, на алёртах оба считаются.
Правило выбора: либо логируй и обработай (catch + return fallback, без re-throw), либо проброс без лога. Не оба сразу.
R-ERR-LOG-X2: log.error(e.getMessage()) без объекта e.
// ПЛОХО
log.error("Failed: " + e.getMessage()); // ← stacktrace потерян
Только message — без stacktrace. Получаем строку "Failed: relation \"orders\" does not exist", без где это произошло, через какой call chain, с какими параметрами.
Правильно:
// ХОРОШО — последний аргумент без {} — SLF4J включит stacktrace
log.error("Failed to register payment for order {}", cmd.orderId(), e);
SLF4J/Logback узнаёт, что последний аргумент — это Throwable (не плейсхолдер для {}), и автоматически добавит stacktrace к выводу. Это то, что нам нужно для ERROR-логов.
Куда дальше
- Error Handling Style Guide → раздел 4. Логирование исключений — нормативные формулировки.
- Observability Style Guide → R-OBS-LOG-* / R-OBS-MDC-* — общий формат логов, MDC, structured JSON.
- Где throw, где catch — почему ноль catch в handler/service.
- Observability ошибок — метрики и алёрты.