Skip to content

State Listeners and Metrics

Frody edited this page Sep 3, 2026 · 1 revision

State Listeners & Observability

Fuse exposes a clean callback listener API to notify monitoring, logging, and metrics systems whenever a circuit breaker changes its operational state.


The EventListener Interface

Register one or more state transition listeners using onStateChange:

circuitBreaker.onStateChange((name, fromState, toState) -> {
    // Invoked whenever the circuit transitions (e.g., CLOSED -> OPEN)
});

Safety Guarantees

  • Thread Confinement Safety: Listeners are stored internally in a lock-free java.util.concurrent.CopyOnWriteArrayList. Adding or executing listeners is completely thread-safe.
  • Fault Isolation: If a registered listener throws an unhandled RuntimeException, Fuse catches and swallows the exception internally. A faulty monitoring callback will never interrupt or break the client's business transaction.

Production Integration Recipes

1. Structured Logging with SLF4J / Logback

Emit structured log records whenever the circuit degrades or recovers:

circuitBreaker.onStateChange((name, fromState, toState) -> {
    if (toState == CircuitBreakerState.OPEN) {
        log.error("ALERT: Circuit breaker '{}' TRIPPED from {} to {}. Downstream calls will be isolated.",
                name, fromState, toState);
    } else if (toState == CircuitBreakerState.CLOSED) {
        log.info("RECOVERED: Circuit breaker '{}' RESTORED from {} to {}. Normal traffic resumed.",
                name, fromState, toState);
    } else {
        log.warn("PROBING: Circuit breaker '{}' entered {}. Testing downstream recovery.",
                name, toState);
    }
});

2. Micrometer & Prometheus Metric Export

In Spring Boot or Micrometer-enabled applications, track the circuit breaker's state as a Prometheus gauge or state tag:

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class CircuitBreakerMetricsBinder {

    public void bind(CircuitBreaker cb, MeterRegistry registry) {
        // Map states to numeric values for Prometheus: 0 = CLOSED, 1 = HALF_OPEN, 2 = OPEN
        AtomicInteger stateGauge = new AtomicInteger(stateToNumber(cb.getState()));

        registry.gauge("circuitbreaker.state",
                Tags.of("name", cb.getName()),
                stateGauge,
                AtomicInteger::get);

        cb.onStateChange((name, from, to) -> {
            stateGauge.set(stateToNumber(to));
            registry.counter("circuitbreaker.transitions.total",
                    Tags.of("name", name, "from", from.name(), "to", to.name()))
                    .increment();
        });
    }

    private int stateToNumber(CircuitBreakerState state) {
        return switch (state) {
            case CLOSED -> 0;
            case HALF_OPEN -> 1;
            case OPEN -> 2;
        };
    }
}

3. Spring Boot Actuator Health Indicator

Expose circuit breaker status in /actuator/health:

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;

public class CircuitBreakerHealthIndicator implements HealthIndicator {

    private final CircuitBreaker circuitBreaker;

    public CircuitBreakerHealthIndicator(CircuitBreaker circuitBreaker) {
        this.circuitBreaker = circuitBreaker;
    }

    @Override
    public Health health() {
        CircuitBreakerState state = circuitBreaker.getState();
        return switch (state) {
            case CLOSED -> Health.up()
                    .withDetail("circuit", circuitBreaker.getName())
                    .withDetail("state", state)
                    .build();
            case HALF_OPEN -> Health.status("DEGRADED")
                    .withDetail("circuit", circuitBreaker.getName())
                    .withDetail("state", state)
                    .build();
            case OPEN -> Health.down()
                    .withDetail("circuit", circuitBreaker.getName())
                    .withDetail("state", state)
                    .build();
        };
    }
}