Опирается на правила: R-RES-OBS-1R-RES-OBS-3 и R-RES-OBS-X1 из Resilience Style Guide → раздел 12. Observability.

Важно знать

  • Micrometer metrics через resilience4j-micrometer dependency. Автоматически экспортирует resilience4j_circuitbreaker_state, _calls, _retry_calls, _bulkhead_available_concurrent_calls.
  • OTel-spans на adapter-методах с атрибутами circuit_breaker.state и external.system. Даёт связку «slow trace → CB был half-open».
  • Structured logging. При каждом state-transition CB — лог уровня WARN с system, prev_state, new_state, failure_rate. Не на каждый успешный call.
  • Отключение R4J metrics без причины — антипаттерн: без них SRE не увидит «CB Sber стабильно half-open» до жалоб клиентов.
  • Минимальный grafana-дашборд: circuitbreaker_state (timeseries по системам), circuitbreaker_calls rate, bulkhead_available, latency p95 outbound.
  • Алёрты: CB в open > 5 минут; bulkhead_available < 2 стабильно; retry_calls{kind=failed_with_retry} rate растёт.

Защита от отказов бесполезна, если о её срабатываниях никто не узнаёт. Resilience4j из коробки даёт всё необходимое для наблюдаемости через Micrometer и OTel; нужно только подключить и не отключить. Раскрытие раздела 12 гайда.

Micrometer-метрики через resilience4j-micrometer

R-RES-OBS-1: подключение dependency автоматически регистрирует метрики во всех MeterRegistry бинах (в т.ч. Prometheus).

// build.gradle.kts
dependencies {
    implementation("io.github.resilience4j:resilience4j-spring-boot3:2.2.0")
    implementation("io.github.resilience4j:resilience4j-micrometer:2.2.0")
    implementation("io.micrometer:micrometer-registry-prometheus")
}

Что появляется в /actuator/prometheus:

# CircuitBreaker
resilience4j_circuitbreaker_state{name="sber",state="closed"} 1.0
resilience4j_circuitbreaker_state{name="sber",state="open"} 0.0
resilience4j_circuitbreaker_state{name="sber",state="half_open"} 0.0
resilience4j_circuitbreaker_calls_total{name="sber",kind="successful"} 1234
resilience4j_circuitbreaker_calls_total{name="sber",kind="failed"} 42
resilience4j_circuitbreaker_calls_total{name="sber",kind="ignored"} 0
resilience4j_circuitbreaker_failure_rate{name="sber"} 3.4
resilience4j_circuitbreaker_slow_call_rate{name="sber"} 1.2

# Retry
resilience4j_retry_calls_total{name="sber",kind="successful_without_retry"} 1100
resilience4j_retry_calls_total{name="sber",kind="successful_with_retry"} 80
resilience4j_retry_calls_total{name="sber",kind="failed_without_retry"} 12
resilience4j_retry_calls_total{name="sber",kind="failed_with_retry"} 30

# Bulkhead
resilience4j_bulkhead_available_concurrent_calls{name="sber"} 14
resilience4j_bulkhead_max_allowed_concurrent_calls{name="sber"} 16

Что отсюда строится в Grafana / дашборд:

  • Per-system панель: текущий state CB, calls/s success vs failed, current failure rate.
  • Bulkhead utilization: (max - available) / max — насколько забит лимит.
  • Retry funnel: успешно с первой попытки / успешно с retry / провалилось.

OTel-spans с атрибутами

R-RES-OBS-2: при каждом adapter-вызове создаётся span с атрибутами CB-state.

@Component
@RequiredArgsConstructor
public class SberClientAdapter implements PaymentPort {

    private final Tracer tracer;
    private final CircuitBreakerRegistry cbRegistry;

    @CircuitBreaker(name = "sber", fallbackMethod = "registerFallback")
    @Bulkhead(name = "sber")
    @Retry(name = "sber")
    public RegisterResult register(RegisterCommand cmd) {
        Span span = tracer.spanBuilder("sber.register")
            .setAttribute("external.system", "sber")
            .setAttribute("circuit_breaker.state",
                cbRegistry.circuitBreaker("sber").getState().toString())
            .startSpan();
        try (Scope s = span.makeCurrent()) {
            return doRegister(cmd);
        } catch (Exception e) {
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

Эту обвязку можно упростить через @Observed аннотацию (Spring 6.1+) — Spring Observation API сделает то же автоматически:

@Observed(
    name = "sber.register",
    contextualName = "sber-register",
    lowCardinalityKeyValues = {"external.system", "sber"}
)
@CircuitBreaker(name = "sber", fallbackMethod = "registerFallback")
public RegisterResult register(RegisterCommand cmd) { ... }

Что даёт связь с трейсингом:

  • В Jaeger / Tempo видно «slow trace 5s → span sber.register с circuit_breaker.state=half_open» — сразу понятно, что CB был на грани открытия.
  • Алёрт «slow outbound» можно decorировать атрибутами для root-cause анализа.

Structured logging при state-transition

R-RES-OBS-3: лог уровня WARN на переходах CB-state, не на каждый call.

@Configuration
@RequiredArgsConstructor
public class ResilienceLogConfig {

    @EventListener
    public void onCbEvent(CircuitBreakerOnStateTransitionEvent event) {
        var t = event.getStateTransition();
        log.warn("circuit breaker {} state changed: {} → {} (failure rate {}%)",
            event.getCircuitBreakerName(),
            t.getFromState(),
            t.getToState(),
            event.getCircuitBreaker().getMetrics().getFailureRate()
        );
    }
}

Что важно:

  • Только transitions (CLOSED → OPEN, OPEN → HALF_OPEN, HALF_OPEN → CLOSED). Не каждый успешный call.
  • WARN-уровень. ERROR — слишком крикливо; INFO — слишком тихо.
  • Достаточно атрибутов: имя системы, prev/new state, failure rate. SRE сразу видит причину.
  • Через CircuitBreakerEventListener — Resilience4j публикует события автоматически, нужно только подписаться.

Аналогично можно подписаться на BulkheadOnCallRejectedEvent (WARN) и RetryOnRetryEvent (DEBUG/INFO).

Что запрещено

Отключение R4J metrics без причины

R-RES-OBS-X1: management.metrics.enable.resilience4j=false или resilience4j.circuitbreaker.configs.default.register-health-indicator=false без обоснования.

# ПЛОХО — отключили метрики
management.metrics.enable.resilience4j: false

Что произойдёт:

  • На графиках Prometheus нет линий по circuitbreaker_state. SRE не видит, что CB Sber стабильно half_open.
  • При деградации Sber единственный сигнал — жалоба клиента «у меня платёж не проходит». 30 минут на расследование вместо 30 секунд по графику.
  • Алёрты типа «CB в open > 5 минут» не работают — нет данных.

Корректно: оставить включённым (default). Если объём метрик беспокоит — отфильтровать на стороне Prometheus, не отключать в источнике.

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
management.metrics.enable.resilience4j=false без причиныR-RES-OBS-X1Оставить включённым
register-health-indicator=false для CBR-RES-HC-4Включить, чтобы /actuator/health показывал CB-состояние
Лог на каждый успешный callR-RES-OBS-3Только на state-transitions
ERROR-уровень для CB transitionR-RES-OBS-3WARN
Span без атрибутов external.system и circuit_breaker.stateR-RES-OBS-2Атрибуты для root-cause analysis
Только metrics без алёртовR-RES-OBS-1Алёрты на CB-state, bulkhead utilization, retry-failure rate

Куда дальше

  • Resilience → раздел 12. Observability — нормативные R-RES-OBS-*.
  • Circuit Breaker — state transitions и их semantics.
  • Bulkhead — available_concurrent_calls метрика.
  • Retry — retry_calls{kind=...} метрики.
  • Health checks — Actuator endpoints.
  • Observability Style Guide — общий гайд по metrics, logs, tracing.
  • Конфигурация — register-health-indicator per-instance.