Опирается на правила:
R-ERR-OBS-1…R-ERR-OBS-3иR-ERR-OBS-X1из Error Handling Style Guide → раздел 7. Observability.
Важно знать
- Метрика
app_errors_total{type=..., exception=...}(Counter) — основа дашборда ошибок.type∈ {domain,validation,integration,technical,unexpected};exception— simple class name.- OpenTelemetry span помечается как ERROR на исключение:
SpanStatus.ERROR+recordException(e). В Spring-инструментации делается автоматически.- Алёрты — на необычные паттерны, не на каждый exception.
unexpectedрастёт = новый баг;integrationрастёт = внешка деградирует;domainстабилен (рост = бизнес-условие изменилось);validationстабилен (рост = клиент сломал контракт).- Запрещено: алёрт «любое исключение в логах». Шум —
DomainExceptionнормально, частая.- Алёртить только на
unexpected/technical— то, что не должно случаться.
ERROR-логи + метрики + traces — это три источника правды для observability ошибок. Каждый отвечает на свой вопрос: метрика — «сколько и какого типа», trace — «в каком запросе, через какую цепочку», лог — «детали для разбора». Алёрт строится на метрике (быстрее и стабильнее, чем log-grep), на нужных категориях. Раскрытие правил R-ERR-OBS-* ниже.
Метрика app_errors_total
R-ERR-OBS-1: Counter с двумя тэгами.
@Component
@RequiredArgsConstructor
public class ErrorMetrics {
private final MeterRegistry registry;
public void recordError(Throwable ex, String type) {
Counter.builder("app.errors.total")
.tag("type", type)
.tag("exception", ex.getClass().getSimpleName())
.register(registry)
.increment();
}
}
Используется в @RestControllerAdvice:
@ExceptionHandler(DomainException.class)
public ResponseEntity<ProblemDetail> handleDomain(DomainException ex) {
errorMetrics.recordError(ex, "domain");
log.warn("Domain rule violated: {}", ex.getMessage(), ex);
// ...
}
@ExceptionHandler(IntegrationException.class)
public ResponseEntity<ProblemDetail> handleIntegration(IntegrationException ex) {
errorMetrics.recordError(ex, "integration");
// ...
}
@ExceptionHandler(Throwable.class)
public ResponseEntity<ProblemDetail> handleUnexpected(Throwable ex) {
errorMetrics.recordError(ex, "unexpected");
// ...
}
Values:
Tag type | Что |
|---|---|
domain | DomainException и наследники |
validation | ValidationException, MethodArgumentNotValidException |
integration | IntegrationException и наследники |
technical | TechnicalException |
unexpected | catch-all Throwable (ничего из иерархии) |
Tag exception — simple class name (не FQDN): InsufficientFundsException, SberRegisterException. Это даёт низкую cardinality (несколько десятков значений в худшем случае), Prometheus справляется без проблем.
Не добавляем в tag-ы поля типа customerId, orderId — это высокая cardinality, кардинально ломает Prometheus. Эти данные — в ProblemDetails.properties и в логах.
OpenTelemetry — span как ERROR
R-ERR-OBS-2: trace-span помечается на исключение.
При Spring Boot + Spring Cloud Sleuth / OpenTelemetry-инструментации это делается автоматически для исключений, долетевших до @RestControllerAdvice или до конца HTTP-обработки. В trace в Jaeger / Tempo span будет показан красным, с прикреплённым stacktrace в attributes.
Если ловишь исключение внутри handler'а и хочешь зафиксировать в trace без re-throw (что разрешено только в out-adapter с fallback'ом):
@Component
@RequiredArgsConstructor
public class SberClientAdapter implements PaymentPort {
private final Tracer tracer; // OpenTelemetry tracer
@Override
@CircuitBreaker(name = "sber", fallbackMethod = "registerFallback")
public RegisterResult register(RegisterCommand cmd) {
// ...
}
private RegisterResult registerFallback(RegisterCommand cmd, CallNotPermittedException ex) {
var span = Span.current();
span.setStatus(StatusCode.ERROR, "Sber CB open");
span.recordException(ex);
throw new SberUnavailableException(cmd.orderId(), ex);
}
}
На практике автоматики Spring + OpenTelemetry достаточно — мануальный recordException нужен редко.
Алёрты — на паттерны, не на отдельные exceptions
R-ERR-OBS-3: что алёртить, что не алёртить.
Алёртим:
- Резкий рост
unexpected(catch-all). Это сигнал нового бага — появился тип исключения, не описанный в иерархии. Создавать issue, добавлять тип, делать unit-test. - Рост
integrationдля конкретной системы. Внешка деградирует — но в этом случае алёрт лучше делать через метрику Resilience4jcircuitbreaker_state(быстрее).app_errors_total{type="integration", exception="SberRegisterException"}— комплементарный сигнал. - Рост
domainдля одного типа. Например,InsufficientFundsExceptionобычно даёт 50 в час; вдруг 500. Это бизнес-сигнал — возможно изменилось бизнес-условие или конкурент уронил курс, и пользователи массово упёрлись в правило. Команду продукта надо предупредить. - Любой рост
technical. Это инфраструктурный сбой, требует внимания.
Не алёртим:
- Стабильный baseline
domain. Это нормальная нагрузка — каждый день есть N попыток списать с пустой карты. - Стабильный baseline
validation. Клиенты иногда шлют плохой input — нормально. - На каждое отдельное исключение в логах. Это shutdown algorithm для on-call.
Что запрещено
R-ERR-OBS-X1: алёрт «любое исключение в логах».
Конфиг алёрта типа:
# ПЛОХО
sum(rate(app_errors_total[5m])) > 0
Это сработает на любой DomainException (нормально), ValidationException (нормально), любую другую ошибку. On-call дежурный получит 50 SMS в час, отключит алёрты, и настоящие инциденты будут пропущены.
Правильно — алёртить только на категории, которые не должны случаться, или их рост:
# ХОРОШО — резкий рост unexpected
rate(app_errors_total{type="unexpected"}[5m]) > 0.5 # больше 0.5 событий/сек
# ХОРОШО — CB открылся
sum by (name) (resilience4j_circuitbreaker_state{state="open"}) > 0
# ХОРОШО — рост technical
rate(app_errors_total{type="technical"}[5m])
> 3 * rate(app_errors_total{type="technical"}[1h])
# скорость за 5 минут в 3 раза выше, чем за час — внутренняя проблема
Каждый алёрт — это сигнал to-act. Если on-call не должен ничего делать, увидев алёрт — это шум, не алёрт.
Куда дальше
- Error Handling Style Guide → раздел 7. Observability — нормативные формулировки.
- Observability Style Guide — общий гайд по метрикам, tracing'у, MDC.
- Логирование исключений — почему
DomainException— WARN, не ERROR. - Resilience Style Guide → R-RES-CB-* — про CircuitBreaker и его метрики.