Reputation: 685
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.
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?
Upvotes: 0
Views: 6433
Reputation: 4691
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.
Upvotes: 0