0

I have 2 services A & B. A is in front of B in deployment and has exact same API as B and just proxies requests to B and returns responses back from B to the caller. B has many other services connected to it and its health endpoint shows health of all those services along with its own.

A & B are both spring boot 2.2.6 services.

Requirement : When the health endpoint of A is called (which is same as that of B) it should return the health exactly as shown by B. Basically pass the health request to B and return B's response back to the caller.

So the question is how do we override the Spring managed actuator health endpoint of A so that instead of it showing its health it calls B and returns the response. Standard spring boot health indicator wont work in this case of course.

So far its seems that class annotated with @EndpointWebExtension(endpoint = HealthEndpoint.class) can somehow be used to override the default health endpoints behavior however I am getting the following exception :

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pathMappedEndpoints' defined in class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints]: Factory method 'pathMappedEndpoints' threw exception; nested exception is java.lang.IllegalStateException: Unable to map duplicate endpoint operations: [web request predicate GET to path 'health' produces: application/vnd.spring-boot.actuator.v3+json,application/vnd.spring-boot.actuator.v2+json,application/json] to healthEndpoint (applicationHealthWebEndpointExtension)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:656)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:636)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1338)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:882)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
    at com.gi_de.aon360.Aon360FeApplication.main(Aon360FeApplication.java:16)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints]: Factory method 'pathMappedEndpoints' threw exception; nested exception is java.lang.IllegalStateException: Unable to map duplicate endpoint operations: [web request predicate GET to path 'health' produces: application/vnd.spring-boot.actuator.v3+json,application/vnd.spring-boot.actuator.v2+json,application/json] to healthEndpoint (applicationHealthWebEndpointExtension)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651)
    ... 19 more
Caused by: java.lang.IllegalStateException: Unable to map duplicate endpoint operations: [web request predicate GET to path 'health' produces: application/vnd.spring-boot.actuator.v3+json,application/vnd.spring-boot.actuator.v2+json,application/json] to healthEndpoint (applicationHealthWebEndpointExtension)
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.assertNoDuplicateOperations(EndpointDiscoverer.java:231)
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.convertToEndpoint(EndpointDiscoverer.java:198)
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.convertToEndpoints(EndpointDiscoverer.java:179)
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.discoverEndpoints(EndpointDiscoverer.java:124)
    at org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer.getEndpoints(EndpointDiscoverer.java:116)
    at org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints.lambda$getEndpoints$1(PathMappedEndpoints.java:69)
    at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608)
    at org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints.getEndpoints(PathMappedEndpoints.java:68)
    at org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints.<init>(PathMappedEndpoints.java:63)
    at org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration.pathMappedEndpoints(WebEndpointAutoConfiguration.java:111)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
    ... 20 more

So how do we override the health endpoint? Any example, documentation link etc would helpful.

Update/Edit:

Overriding the default HealthWebExtension seems tricky. So I found a simple workaround. Let me know your thoughts on it: I simply disabled springs default healthendpoint in the application.properties file by using the property : management.endpoint.health.enabled=false and provide my own restcontroller with @GetMapping("/manage/health") which basically calls the service B and returns the health response.

I don't quiet like this solution because first of all its a bit misleading that the health is disabled in the properties file and still the endpoint works. Second if I make the property true it simply overrides my endpoint without any error or warning.

Is there a way to make spring boot pick my custom restcontroller, if the health property is set to true instead of the default one?

humbleCoder
  • 489
  • 4
  • 16
  • Does it serve your purpose? https://stackoverflow.com/questions/44849568/how-to-add-a-custom-health-check-in-spring-boot-health – pinkpanther Oct 08 '20 at 16:48
  • No it does not. However the last anwser does say that "health endpoint is a bit trickier to extend because one extension for it is provided out of the box by actuator itself". This is the reason why I am getting the exception. Not sure how to get rid of it though. – humbleCoder Oct 08 '20 at 17:03
  • how about the last answer in that question? https://stackoverflow.com/a/44849702/2354099 – pinkpanther Oct 08 '20 at 17:29
  • This following answer has some mentioned the problem I am facing but not the solution. That person has used a different endpoint to check health which I am not allowed to do: https://stackoverflow.com/questions/44849568/how-to-add-a-custom-health-check-in-spring-boot-health/44849702#44849702 – humbleCoder Oct 09 '20 at 04:13
  • Pasted the incorrect link. I was actually talking about this answer : https://stackoverflow.com/a/51775227/1711670 – humbleCoder Oct 09 '20 at 04:51

1 Answers1

0

What about the case where service A is down? You make a call to service A and the response does not include the status of A. There is a better way to go by implementing the HealthIndicator interface. Something like :

@Component
public class ServiceBHealthIndicator implements HealthIndicator {

   
    @Override
    public Health health() {

        ResponseEntity<Map> response = null;

        try {
            response = WebClient.create( "url to B/health").get()
                    .retrieve()
                    .toEntity( Map.class ).block();
        } catch ( Exception e ) {
            log.error( "B is down", e );
        }

        Health health = Health.down().withDetail( "reason", "Service B does not respond" ).build();

        if ( Objects.nonNull( response ) && response.getStatusCode().is2xxSuccessful() ) {
            health = Health.up()
                    .withDetail( "reason", "Service B is up" )
                    .withDetails( response.getBody() )
                    .build();
        }
        return health;
    }
}

With this solution, you will get both : A and B health status.

akuma8
  • 2,822
  • 1
  • 25
  • 55
  • Thanks for the suggestion will have to check if its ok to use this. One more thing do you know how to get nested components response as shown in the spring boot documentation : https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/actuator-api/html/#health-retrieving – humbleCoder Oct 09 '20 at 10:32
  • Nested components can be considered as `Map` of `Map`, something like `Map>` – akuma8 Oct 09 '20 at 17:38
  • I mean not the json structure but how are indicators used to get that. Generally we have indicator extending AbstractIndicator for each downstream service. The indicator class name prefix shows up in the components part of the health response. Not sure however how to arrange the indicators to get the health of the all downstream services. – humbleCoder Oct 10 '20 at 13:37
  • It's a new question not related with this one, please ask a new one. – akuma8 Oct 11 '20 at 14:13
  • That's fine got the solution :-) : https://stackoverflow.com/questions/51861320/how-to-aggregate-health-indicators-in-spring-boot – humbleCoder Oct 12 '20 at 04:03