We are using org.springframework.boot:spring-boot-actuator:3.2.7 and you cannot replace the ReactiveHealthEndpointWebExtension as shown in other answers. We tried this and it failed with
Caused by: java.lang.IllegalStateException: Found multiple extensions for the endpoint bean healthEndpoint (loggingHealthEndpointWebExtension, reactiveHealthEndpointWebExtension) at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.convertToEndpoint(EndpointDiscoverer.java:198) at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.convertToEndpoints(EndpointDiscoverer.java:182)
There are more clues in here : https://github.com/spring-projects/spring-boot/issues/22632 about replacing or not.
However Spring does have a OperationInvokerAdvisor interception mechanism, which while archaic does work.
Here is an example based on some of the code above which logs when the health check fails and logs what has failed
/** * Spring does not log when a health check fails. * <p> * So they can fail, and it will send a 503 but we cant know which health indicator * failed or why. This class allows us to intercept the health check actuator * response and log if it's failed. * <p> * This is esoteric code, and it's not easily understood, but it works, and it does use * official Spring {@link OperationInvokerAdvisor} classes. * */ @Component public class HealthLoggingAdvisor implements OperationInvokerAdvisor { private final Logger log = LoggerFactory.getLogger("HealthCheck"); @Override public OperationInvoker apply(EndpointId endpointId, OperationType operationType, OperationParameters parameters, OperationInvoker invoker) { if (endpointId.toLowerCaseString().equals("health")) { return context -> { Object invoked = invoker.invoke(context); if (invoked instanceof Mono<?> mono) { //noinspection ReactiveStreamsUnusedPublisher return mono.doOnNext(monoResult -> logIfNotHealthy(monoResult, context)); } return invoked; }; } return invoker; } private void logIfNotHealthy(Object monoResult, InvocationContext context) { try { if (monoResult instanceof WebEndpointResponse) { @SuppressWarnings("unchecked") WebEndpointResponse<? extends HealthComponent> response = (WebEndpointResponse<? extends HealthComponent>) monoResult; HealthComponent health = response.getBody(); if (health == null) { return; } Status status = health.getStatus(); if (status != Status.UP) { log.warn("Health endpoint {} failing - it returned {}. components={}", kv("path", getPath(context)), kv("status", status), kv("components", getComponents(health))); } } } catch (RuntimeException rte) { // let's not stop a request because our code has a bug log.warn("Unable to log health status - continuing...", rte); } } private static @NotNull Map<String, HealthComponent> getComponents(HealthComponent health) { Map<String, HealthComponent> components = new TreeMap<>(); if (health instanceof CompositeHealth) { Map<String, HealthComponent> details = ((CompositeHealth) health).getComponents(); if (details != null) { components.putAll(details); } } return components; } private static @Nullable Object getPath(InvocationContext context) { Object path = null; Map<String, Object> arguments = context.getArguments(); if (arguments != null) { path = arguments.get("path"); } return path; } }
This code is Reactive but I suspect it would also work for Spring MVC with some tweaks.