Custom Health Indicator

Checking a Dependency

ShopEase cannot take orders if the payment gateway is down. A custom HealthIndicator adds that check to /actuator/health.

@Component("paymentGateway")
@RequiredArgsConstructor
public class PaymentGatewayHealthIndicator implements HealthIndicator {
    private final RestClient paymentClient;

    @Override
    public Health health() {
        try {
            long start = System.currentTimeMillis();
            paymentClient.get().uri("/v1/health").retrieve().toBodilessEntity();
            return Health.up().withDetail("provider", "razorpay")
                         .withDetail("latencyMs", System.currentTimeMillis() - start).build();
        } catch (Exception e) {
            return Health.down(e).withDetail("provider", "razorpay").build();
        }
    }
}

Example

When the gateway times out the overall status becomes DOWN and the endpoint returns 503:

curl -i localhost:8081/actuator/health

Example output

HTTP/1.1 503 Service Unavailable
{"status":"DOWN","components":{"db":{"status":"UP"},"paymentGateway":{"status":"DOWN","details":{"provider":"razorpay","error":"org.springframework.web.client.ResourceAccessException: I/O error on GET request: Read timed out"}}}}

How Status Is Aggregated

Example

The overall status is the "worst" status of all components. Spring's default order is DOWN, OUT_OF_SERVICE, UP, UNKNOWN:

import java.util.*;

public class Main {
    enum Status { DOWN, OUT_OF_SERVICE, UP, UNKNOWN }     // most severe first

    static Status aggregate(Map<String, Status> components) {
        return components.values().stream().min(Comparator.naturalOrder()).orElse(Status.UNKNOWN);
    }

    public static void main(String[] args) {
        Map<String, Status> healthy = new LinkedHashMap<>();
        healthy.put("db", Status.UP);
        healthy.put("redis", Status.UP);
        healthy.put("paymentGateway", Status.UP);
        System.out.println(healthy + " -> " + aggregate(healthy));

        healthy.put("paymentGateway", Status.DOWN);
        System.out.println(healthy + " -> " + aggregate(healthy));
    }
}

Output

{db=UP, redis=UP, paymentGateway=UP} -> UP
{db=UP, redis=UP, paymentGateway=DOWN} -> DOWN

Liveness vs Readiness

ProbeQuestionIf it fails
/actuator/health/livenessis the process stuck?Kubernetes restarts the container
/actuator/health/readinesscan it serve traffic now?load balancer stops sending requests

Tip: Do not put external dependencies (payment gateway) in the liveness group: a gateway outage would restart every pod and make things worse. Put them in readiness or keep them informational.

Example

application.yml

management:
  endpoint:
    health:
      group:
        readiness:
          include: readinessState,db,redis