12

I am using Spring boot Actuator API for my project the having a health check endpoint, and enabled it by :

management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=healthcheck 

Mentioned here

Now I want to enable log in my application log file when ever the status of this above /healthcheck fails and print the entire response from this end point.

What is the correct way to achieve this?

0

5 Answers 5

3
+50

Best way is to extend the actuator endpoint with @EndpointWebExtension. You can do the following;

@Component @EndpointWebExtension(endpoint = HealthEndpoint.class) public class HealthEndpointWebExtension { private HealthEndpoint healthEndpoint; private HealthStatusHttpMapper statusHttpMapper; // Constructor @ReadOperation public WebEndpointResponse<Health> health() { Health health = this.healthEndpoint.health(); Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); // log here depending on health status. return new WebEndpointResponse<>(health, status); } } 

More about actuator endpoint extending here, at 4.8. Extending Existing Endpoints

Sign up to request clarification or add additional context in comments.

3 Comments

In spring-boot-actuator 2.2.5, the Extension to HealthEndpoint.class seem to already exist/defined in org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration as a Bean of type org.springframework.boot.actuate.health.HealthEndpointWebExtension . Not allowing to have more than one Extension for the same HealthEndpoint. The spring boot app does not start. Did this code work for anyone?
@madhav-turangi I managed to do this by extending ReactiveHealthEndpointWebExtension(registry, groups). Then adding the annotations as above.
Does the above code get executed when you view [myhost:port]/actuator/health from a browser? It doesn't for me. Is there another way I should get this to execute when testing it?
3

The above answers did not work for me. I implemented the below and it works. When you view [myhost:port]/actuator/health from your browser the below will execute. You can also add healthCheckLogger to your readiness/liveness probes so it executes periodically.

@Slf4j @Component public class HealthCheckLogger implements HealthIndicator { @Lazy @Autowired private HealthEndpoint healthEndpoint; @Override public Health health() { log.info("DB health: {}", healthEndpoint.healthForPath("db")); log.info("DB health: {}", healthEndpoint.healthForPath("diskSpace")); return Health.up().build(); } } 

Comments

2

Extending the HealthEndpoint using a EndpointWebExtension does not work with newer Spring versions. It's not allowed to override the existing (web-) extension or re-register another one.

Another solution is using a Filter. The following implementation logs if the health check fails:

public class HealthLoggingFilter implements Filter { private static final Logger LOG = LoggerFactory.getLogger(HealthLoggingFilter.class); @Override public void init(FilterConfig filterConfig) { // nothing to do } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ContentCachingResponseWrapper responseCacheWrapperObject = new ContentCachingResponseWrapper((HttpServletResponse) response); chain.doFilter(request, responseCacheWrapperObject); int status = ((HttpServletResponse) response).getStatus(); if (status >= 400) { // unhealthy byte[] responseArray = responseCacheWrapperObject.getContentAsByteArray(); String responseStr = new String(responseArray, responseCacheWrapperObject.getCharacterEncoding()); LOG.warn("Unhealthy. Health check returned: {}", responseStr); } responseCacheWrapperObject.copyBodyToResponse(); } @Override public void destroy() { // nothing to do } } 

The Filter can be registered for the actuator/health route using FilterRegistrationBean:

@Bean public FilterRegistrationBean<HealthLoggingFilter > loggingFilter(){ FilterRegistrationBean<HealthLoggingFilter > registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new HealthLoggingFilter ()); registrationBean.addUrlPatterns("/actuator/health"); return registrationBean; } 

Comments

1

If using Webflux this worked for me sample in Kotlin

@Component @EndpointWebExtension(endpoint = HealthEndpoint::class) class LoggingReactiveHealthEndpointWebExtension( registry: ReactiveHealthContributorRegistry, groups: HealthEndpointGroups ) : ReactiveHealthEndpointWebExtension(registry, groups) { companion object { private val logger = LoggerFactory.getLogger(LoggingReactiveHealthEndpointWebExtension::class.java) } override fun health( apiVersion: ApiVersion?, securityContext: SecurityContext?, showAll: Boolean, vararg path: String? ): Mono<WebEndpointResponse<out HealthComponent>> { val health = super.health(apiVersion, securityContext, showAll, *path) return health.doOnNext { if (it.body.status == UP) { logger.info("Health status: {}, {}", it.body.status, ObjectMapper().writeValueAsString(it.body)) } } } } 

Comments

1

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.