Merge pull request #509 from jmnarloch/eureka-healthcheck
* eureka-healthcheck: Eureka health check
This commit is contained in:
@@ -95,6 +95,28 @@ These links show up in the metadata that is consumed by clients, and
|
||||
used in some scenarios to decide whether to send requests to your
|
||||
application, so it's helpful if they are accurate.
|
||||
|
||||
=== Eureka's Health Checks
|
||||
|
||||
By default, Eureka uses the client heartbeat to determine if a client is up.
|
||||
Unless specified otherwise the Discovery Client will not propagate the
|
||||
application Spring Boot Actuator current health check status. Which means
|
||||
that after successful registration Eureka will always announce that the
|
||||
application is in 'UP' state. This behaviour can be altered by enabling
|
||||
Eureka health checks, which results in propagating application status
|
||||
to Eureka, as a consequence every other application will not sending
|
||||
traffic to application in state other then 'UP'.
|
||||
|
||||
.application.yml
|
||||
----
|
||||
eureka:
|
||||
client:
|
||||
healthcheck:
|
||||
enabled: true
|
||||
----
|
||||
|
||||
If you require more control over the health checks, you may consider
|
||||
implementing your own `com.netflix.appinfo.HealthCheckHandler`.
|
||||
|
||||
=== Eureka Metadata for Instances and Clients
|
||||
|
||||
It's worth spending a bit of time understanding how the Eureka metadata works, so you can use it in a way that makes sense in your platform. There is standard metadata for things like hostname, IP address, port numbers, status page and health check. These are published in the service registry and used by clients to contact the services in a straightforward way. Additional metadata can be added to the instance registration in the `eureka.instance.metadataMap`, and this will be accessible in the remote clients, but in general will not change the behaviour of the client, unless it is made aware of the meaning of the metadata. There are a couple of special cases described below where Spring Cloud already assigns meaning to the metadata map.
|
||||
|
||||
@@ -25,6 +25,9 @@ import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.endpoint.Endpoint;
|
||||
import org.springframework.boot.actuate.health.HealthAggregator;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
|
||||
import org.springframework.boot.actuate.metrics.reader.CompositeMetricReader;
|
||||
import org.springframework.boot.actuate.metrics.reader.MetricReader;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
@@ -50,6 +53,9 @@ import com.netflix.discovery.EurekaClientConfig;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Spencer Gibb
|
||||
* @author Jon Schneider
|
||||
* @author Jakub Narloch
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties
|
||||
@@ -174,4 +180,18 @@ public class EurekaDiscoveryClientConfiguration implements SmartLifecycle, Order
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "eureka.client.healthcheck.enabled", matchIfMissing = false)
|
||||
@ConditionalOnBean(HealthIndicator.class)
|
||||
protected static class EurekaHealthCheckHandlerConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
private HealthAggregator healthAggregator = new OrderedHealthAggregator();
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(HealthCheckHandler.class)
|
||||
public EurekaHealthCheckHandler eurekaHealthCheckHandler() {
|
||||
return new EurekaHealthCheckHandler(healthAggregator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.netflix.eureka;
|
||||
|
||||
import static com.netflix.appinfo.InstanceInfo.InstanceStatus;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.HealthAggregator;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.netflix.appinfo.HealthCheckHandler;
|
||||
import com.netflix.appinfo.InstanceInfo;
|
||||
|
||||
/**
|
||||
* A Eureka health checker, maps the application status into {@link InstanceStatus}
|
||||
* that will be propagated to Eureka registry.
|
||||
*
|
||||
* On each heartbeat Eureka performs the health check invoking registered {@link HealthCheckHandler}. By default this
|
||||
* implementation will perform aggregation of all registered {@link HealthIndicator}
|
||||
* through registered {@link HealthAggregator}.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
* @see HealthCheckHandler
|
||||
* @see HealthAggregator
|
||||
*/
|
||||
public class EurekaHealthCheckHandler implements HealthCheckHandler, ApplicationContextAware, InitializingBean {
|
||||
|
||||
private static final Map<Status, InstanceInfo.InstanceStatus> STATUS_MAPPING =
|
||||
new HashMap<Status, InstanceInfo.InstanceStatus>() {{
|
||||
put(Status.UNKNOWN, InstanceStatus.UNKNOWN);
|
||||
put(Status.OUT_OF_SERVICE, InstanceStatus.OUT_OF_SERVICE);
|
||||
put(Status.DOWN, InstanceStatus.DOWN);
|
||||
put(Status.UP, InstanceStatus.UP);
|
||||
}};
|
||||
|
||||
private final CompositeHealthIndicator healthIndicator;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
public EurekaHealthCheckHandler(HealthAggregator healthAggregator) {
|
||||
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
|
||||
this.healthIndicator = new CompositeHealthIndicator(healthAggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
final Map<String, HealthIndicator> healthIndicators = applicationContext.getBeansOfType(HealthIndicator.class);
|
||||
for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
|
||||
healthIndicator.addHealthIndicator(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstanceStatus getStatus(InstanceStatus instanceStatus) {
|
||||
return getHealthStatus();
|
||||
}
|
||||
|
||||
protected InstanceStatus getHealthStatus() {
|
||||
final Status status = healthIndicator.health().getStatus();
|
||||
return mapToInstanceStatus(status);
|
||||
}
|
||||
|
||||
protected InstanceStatus mapToInstanceStatus(Status status) {
|
||||
if (!STATUS_MAPPING.containsKey(status)) {
|
||||
return InstanceStatus.UNKNOWN;
|
||||
}
|
||||
return STATUS_MAPPING.get(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.netflix.eureka;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import com.netflix.appinfo.InstanceInfo;
|
||||
|
||||
/**
|
||||
* Tests the {@link EurekaHealthCheckHandler} with different health indicator registered.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
*/
|
||||
public class EurekaHealthCheckHandlerTests {
|
||||
|
||||
private EurekaHealthCheckHandler healthCheckHandler;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
healthCheckHandler = new EurekaHealthCheckHandler(new OrderedHealthAggregator());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoHealthCheckRegistered() throws Exception {
|
||||
|
||||
InstanceInfo.InstanceStatus status = healthCheckHandler.getStatus(InstanceInfo.InstanceStatus.UNKNOWN);
|
||||
assertEquals(InstanceInfo.InstanceStatus.UNKNOWN, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllUp() throws Exception {
|
||||
|
||||
initialize(UpHealthConfiguration.class);
|
||||
|
||||
InstanceInfo.InstanceStatus status = healthCheckHandler.getStatus(InstanceInfo.InstanceStatus.UNKNOWN);
|
||||
assertEquals(InstanceInfo.InstanceStatus.UP, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDown() throws Exception {
|
||||
|
||||
initialize(UpHealthConfiguration.class, DownHealthConfiguration.class);
|
||||
|
||||
InstanceInfo.InstanceStatus status = healthCheckHandler.getStatus(InstanceInfo.InstanceStatus.UNKNOWN);
|
||||
assertEquals(InstanceInfo.InstanceStatus.DOWN, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknown() throws Exception {
|
||||
|
||||
initialize(FatalHealthConfiguration.class);
|
||||
|
||||
InstanceInfo.InstanceStatus status = healthCheckHandler.getStatus(InstanceInfo.InstanceStatus.UNKNOWN);
|
||||
assertEquals(InstanceInfo.InstanceStatus.UNKNOWN, status);
|
||||
}
|
||||
|
||||
private void initialize(Class<?>... configurations) throws Exception {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(configurations);
|
||||
healthCheckHandler.setApplicationContext(applicationContext);
|
||||
healthCheckHandler.afterPropertiesSet();
|
||||
}
|
||||
|
||||
public static class UpHealthConfiguration {
|
||||
|
||||
@Bean
|
||||
public HealthIndicator upIndicator() {
|
||||
return new AbstractHealthIndicator() {
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) throws Exception {
|
||||
builder.up();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class DownHealthConfiguration {
|
||||
|
||||
@Bean
|
||||
public HealthIndicator upIndicator() {
|
||||
return new AbstractHealthIndicator() {
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) throws Exception {
|
||||
builder.down();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class FatalHealthConfiguration {
|
||||
|
||||
@Bean
|
||||
public HealthIndicator upIndicator() {
|
||||
return new AbstractHealthIndicator() {
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) throws Exception {
|
||||
builder.status("fatal");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.netflix.eureka.healthcheck;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
import com.netflix.appinfo.InstanceInfo;
|
||||
import com.netflix.discovery.DiscoveryClient;
|
||||
|
||||
|
||||
/**
|
||||
* Tests the Eureka health check handler.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = EurekaHealthCheckTests.EurekaHealthCheckApplication.class)
|
||||
@WebAppConfiguration
|
||||
@IntegrationTest({"server.port=0", "eureka.client.healthcheck.enabled=true"})
|
||||
@DirtiesContext
|
||||
public class EurekaHealthCheckTests {
|
||||
|
||||
@Autowired
|
||||
private DiscoveryClient discoveryClient;
|
||||
|
||||
@Test
|
||||
public void shouldRegisterService() {
|
||||
|
||||
InstanceInfo.InstanceStatus status = discoveryClient.getHealthCheckHandler()
|
||||
.getStatus(InstanceInfo.InstanceStatus.UNKNOWN);
|
||||
|
||||
assertNotNull(status);
|
||||
assertEquals(InstanceInfo.InstanceStatus.OUT_OF_SERVICE, status);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@EnableEurekaClient
|
||||
protected static class EurekaHealthCheckApplication {
|
||||
|
||||
@Bean
|
||||
public HealthIndicator healthIndicator() {
|
||||
return new HealthIndicator() {
|
||||
@Override
|
||||
public Health health() {
|
||||
return new Health.Builder().outOfService().build();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user