Allow ConsulHealthIndicator to bypass the services query

Fixes gh-646
This commit is contained in:
bono007
2020-07-12 09:55:13 -05:00
committed by spencergibb
parent 50c74c9eba
commit 4a6d7027f0
9 changed files with 330 additions and 74 deletions

View File

@@ -33,8 +33,8 @@
|spring.cloud.consul.discovery.health-check-url | | Custom health check url to override default.
|spring.cloud.consul.discovery.heartbeat.enabled | false |
|spring.cloud.consul.discovery.heartbeat.interval-ratio | |
|spring.cloud.consul.discovery.heartbeat.ttl-unit | s |
|spring.cloud.consul.discovery.heartbeat.ttl-value | 30 |
|spring.cloud.consul.discovery.heartbeat.ttl-unit | s |
|spring.cloud.consul.discovery.heartbeat.ttl-value | 30 |
|spring.cloud.consul.discovery.hostname | | Hostname to use when accessing server.
|spring.cloud.consul.discovery.include-hostname-in-instance-id | false | Whether hostname is included into the default instance id when registering service.
|spring.cloud.consul.discovery.instance-group | | Service instance group.
@@ -77,4 +77,4 @@
|spring.cloud.consul.tls.key-store-password | | Password to an external keystore.
|spring.cloud.consul.tls.key-store-path | | Path to an external keystore.
|===
|===

View File

@@ -31,7 +31,7 @@ To activate Consul Service Discovery use the starter with group `org.springframe
=== Registering with Consul
When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP https://www.consul.io/docs/agent/checks.html[Check] is created by default that Consul hits the `/health` endpoint every 10 seconds. If the health check fails, the service instance is marked as critical.
When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP https://www.consul.io/docs/agent/checks.html[Check] is created by default that Consul hits the `/actuator/health` endpoint every 10 seconds. If the health check fails, the service instance is marked as critical.
Example Consul client:
@@ -149,9 +149,13 @@ spring.cloud.consul.discovery.management-suffix
spring.cloud.consul.discovery.management-tags
----
=== HTTP Health Check
==== HTTP Health Check
The health check for a Consul instance defaults to "/health", which is the default locations of a useful endpoint in a Spring Boot Actuator application. You need to change these, even for an Actuator application if you use a non-default context path or servlet path (e.g. `server.servletPath=/foo`) or management endpoint path (e.g. `management.server.servlet.context-path=/admin`). The interval that Consul uses to check the health endpoint may also be configured. "10s" and "1m" represent 10 seconds and 1 minute respectively. Example:
The health check for a Consul instance defaults to "/actuator/health", which is the default location of the health endpoint in a Spring Boot Actuator application. You need to change this, even for an Actuator application, if you use a non-default context path or servlet path (e.g. `server.servletPath=/foo`) or management endpoint path (e.g. `management.server.servlet.context-path=/admin`).
The interval that Consul uses to check the health endpoint may also be configured. "10s" and "1m" represent 10 seconds and 1 minute respectively.
This example illustrates the above (see the `spring.cloud.consul.discovery.health-check-*` properties in link:appendix.html[the appendix page] for more options).
.application.yml
----
@@ -159,11 +163,57 @@ spring:
cloud:
consul:
discovery:
healthCheckPath: ${management.server.servlet.context-path}/health
healthCheckPath: ${management.server.servlet.context-path}/actuator/health
healthCheckInterval: 15s
----
You can disable the health check by setting `management.health.consul.enabled=false`.
You can disable the HTTP health check entirely by setting `spring.cloud.consul.discovery.register-health-check=false`.
===== Applying Headers
Headers can be applied to health check requests. For example, if you're trying to register a https://cloud.spring.io/spring-cloud-config/[Spring Cloud Config] server that uses https://github.com/spring-cloud/spring-cloud-config/blob/master/docs/src/main/asciidoc/spring-cloud-config.adoc#vault-backend[Vault Backend]:
.application.yml
----
spring:
cloud:
consul:
discovery:
health-check-headers:
X-Config-Token: 6442e58b-d1ea-182e-cfa5-cf9cddef0722
----
According to the HTTP standard, each header can have more than one values, in which case, an array can be supplied:
.application.yml
----
spring:
cloud:
consul:
discovery:
health-check-headers:
X-Config-Token:
- "6442e58b-d1ea-182e-cfa5-cf9cddef0722"
- "Some other value"
----
==== Actuator Health Indicator(s)
If the service instance is a Spring Boot Actuator application, it may be provided the following Actuator health indicators.
===== DiscoveryClientHealthIndicator
When Consul Service Discovery is active, a https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#health-indicator[DiscoverClientHealthIndicator] is configured and made available to the Actuator health endpoint.
See https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#health-indicator[here] for configuration options.
===== ConsulHealthIndicator
An indicator is configured that verifies the health of the `ConsulClient`.
By default, it retrieves the Consul leader node status and all registered services.
In deployments that have many registered services it may be costly to retrieve all services on every health check.
To skip the service retrieval and only check the leader node status set `spring.cloud.consul.health-indicator.include-services-query=false`.
To disable the indicator set `management.health.consul.enabled=false`.
WARNING: When the application runs in https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#the-bootstrap-application-context[bootstrap context mode] (the default),
this indicator is loaded into the bootstrap context and is not made available to the Actuator health endpoint.
==== Metadata and Consul tags
@@ -221,34 +271,6 @@ spring:
With this metadata, and multiple service instances deployed on localhost, the random value will kick in there to make the instance unique. In Cloudfoundry the `vcap.application.instance_id` will be populated automatically in a Spring Boot application, so the random value will not be needed.
==== Applying Headers to Health Check Requests
Headers can be applied to health check requests. For example, if you're trying to register a https://cloud.spring.io/spring-cloud-config/[Spring Cloud Config] server that uses https://github.com/spring-cloud/spring-cloud-config/blob/master/docs/src/main/asciidoc/spring-cloud-config.adoc#vault-backend[Vault Backend]:
.application.yml
----
spring:
cloud:
consul:
discovery:
health-check-headers:
X-Config-Token: 6442e58b-d1ea-182e-cfa5-cf9cddef0722
----
According to the HTTP standard, each header can have more than one values, in which case, an array can be supplied:
.application.yml
----
spring:
cloud:
consul:
discovery:
health-check-headers:
X-Config-Token:
- "6442e58b-d1ea-182e-cfa5-cf9cddef0722"
- "Some other value"
----
=== Looking up services
==== Using Load-balancer

View File

@@ -71,6 +71,7 @@ public class ConsulAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Endpoint.class)
@EnableConfigurationProperties(ConsulHealthIndicatorProperties.class)
protected static class ConsulHealthConfig {
@Bean
@@ -83,8 +84,9 @@ public class ConsulAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledHealthIndicator("consul")
public ConsulHealthIndicator consulHealthIndicator(ConsulClient consulClient) {
return new ConsulHealthIndicator(consulClient);
public ConsulHealthIndicator consulHealthIndicator(ConsulClient consulClient,
ConsulHealthIndicatorProperties properties) {
return new ConsulHealthIndicator(consulClient, properties);
}
}

View File

@@ -34,18 +34,24 @@ public class ConsulHealthIndicator extends AbstractHealthIndicator {
private ConsulClient consul;
public ConsulHealthIndicator(ConsulClient consul) {
private ConsulHealthIndicatorProperties properties;
public ConsulHealthIndicator(ConsulClient consul,
ConsulHealthIndicatorProperties properties) {
this.consul = consul;
this.properties = properties;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
protected void doHealthCheck(Health.Builder builder) {
final Response<String> leaderStatus = this.consul.getStatusLeader();
final Response<Map<String, List<String>>> services = this.consul
.getCatalogServices(CatalogServicesRequest.newBuilder()
.setQueryParams(QueryParams.DEFAULT).build());
builder.up().withDetail("leader", leaderStatus.getValue()).withDetail("services",
services.getValue());
builder.up().withDetail("leader", leaderStatus.getValue());
if (properties.isIncludeServicesQuery()) {
final Response<Map<String, List<String>>> services = this.consul
.getCatalogServices(CatalogServicesRequest.newBuilder()
.setQueryParams(QueryParams.DEFAULT).build());
builder.withDetail("services", services.getValue());
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2013-2019 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
*
* https://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.consul;
import com.ecwid.consul.v1.ConsulClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.style.ToStringCreator;
/**
* Configuration properties for {@link ConsulHealthIndicator}.
*
* @author Chris Bono
*/
@ConfigurationProperties("spring.cloud.consul.health-indicator")
public class ConsulHealthIndicatorProperties {
/**
* Whether or not the indicator should include a query for all registered services
* during its execution. When set to {@code false} the indicator only uses the lighter
* {@link ConsulClient#getStatusLeader()}. This can be helpful in large deployments
* where the number of services returned makes the operation unnecessarily heavy.
*/
private boolean includeServicesQuery = true;
boolean isIncludeServicesQuery() {
return includeServicesQuery;
}
void setIncludeServicesQuery(boolean includeServicesQuery) {
this.includeServicesQuery = includeServicesQuery;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("includeServicesQuery", this.includeServicesQuery).toString();
}
}

View File

@@ -22,46 +22,107 @@ import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.ConsulRawClient;
import com.ecwid.consul.v1.catalog.CatalogConsulClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = { "spring.cloud.consul.tls.key-store-instance-type=JKS",
"spring.cloud.consul.tls.key-store-path=src/test/resources/server.jks",
"spring.cloud.consul.tls.key-store-password=letmein",
"spring.cloud.consul.tls.certificate-path=src/test/resources/trustStore.jks",
"spring.cloud.consul.tls.certificate-password=change_me" })
@ContextConfiguration(initializers = ConsulTestcontainers.class)
/**
* Auto-configuration integration tests for {@link ConsulAutoConfiguration}.
*
* @author Chris Bono
*/
public class ConsulAutoConfigurationTests {
@Autowired
private ConsulClient consulClient;
private final ApplicationContextRunner appContextRunner = new ApplicationContextRunner()
.withInitializer(new ConsulTestcontainers())
.withConfiguration(AutoConfigurations.of(ConsulAutoConfiguration.class));
@Test
public void defaultConfiguration() {
appContextRunner.run(context -> assertThat(context).hasNotFailed()
.hasSingleBean(ConsulProperties.class).hasSingleBean(ConsulClient.class)
.hasSingleBean(ConsulHealthIndicator.class)
.doesNotHaveBean(ConsulEndpoint.class));
}
@Test
public void consulDisabled() {
appContextRunner.withPropertyValues("spring.cloud.consul.enabled=false")
.run(context -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ConsulProperties.class)
.doesNotHaveBean(ConsulClient.class)
.doesNotHaveBean(ConsulHealthIndicator.class)
.doesNotHaveBean(ConsulEndpoint.class));
}
@Test
public void tlsConfigured() {
CatalogConsulClient client = (CatalogConsulClient) ReflectionTestUtils
.getField(this.consulClient, "catalogClient");
ConsulRawClient rawClient = (ConsulRawClient) ReflectionTestUtils.getField(client,
"rawClient");
HttpTransport httpTransport = (HttpTransport) ReflectionTestUtils
.getField(rawClient, "httpTransport");
assertThat(httpTransport).isInstanceOf(DefaultHttpsTransport.class);
appContextRunner.withPropertyValues(
"spring.cloud.consul.tls.key-store-instance-type=JKS",
"spring.cloud.consul.tls.key-store-path=src/test/resources/server.jks",
"spring.cloud.consul.tls.key-store-password=letmein",
"spring.cloud.consul.tls.certificate-path=src/test/resources/trustStore.jks",
"spring.cloud.consul.tls.certificate-password=change_me").run(context -> {
assertThat(context).hasNotFailed().hasSingleBean(ConsulClient.class);
ConsulClient consulClient = context.getBean(ConsulClient.class);
CatalogConsulClient catalogClient = (CatalogConsulClient) ReflectionTestUtils
.getField(consulClient, "catalogClient");
ConsulRawClient rawClient = (ConsulRawClient) ReflectionTestUtils
.getField(catalogClient, "rawClient");
HttpTransport httpTransport = (HttpTransport) ReflectionTestUtils
.getField(rawClient, "httpTransport");
assertThat(httpTransport).isInstanceOf(DefaultHttpsTransport.class);
});
}
@EnableAutoConfiguration
@SpringBootConfiguration
protected static class TestConfig {
@Test
public void nonActuatorAppGetsNoEndpointOrHealthIndicator() {
appContextRunner.withClassLoader(new FilteredClassLoader(Endpoint.class))
.withPropertyValues("management.endpoints.web.exposure.include=consul")
.run(context -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ConsulHealthIndicator.class)
.doesNotHaveBean(ConsulEndpoint.class));
}
@Test
public void consulEndpointAvailable() {
appContextRunner
.withPropertyValues("management.endpoints.web.exposure.include=consul")
.run(context -> assertThat(context).hasNotFailed()
.hasSingleBean(ConsulEndpoint.class));
}
@Test
public void consulEndpointAvailableButDisabled() {
appContextRunner
.withPropertyValues("management.endpoints.web.exposure.include=consul",
"management.endpoint.consul.enabled=false")
.run(context -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ConsulEndpoint.class));
}
@Test
public void consulEndpointDisabled() {
appContextRunner.withPropertyValues("spring.cloud.consul.enabled=false")
.run(context -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ConsulProperties.class)
.doesNotHaveBean(ConsulClient.class)
.doesNotHaveBean(ConsulHealthIndicator.class)
.doesNotHaveBean(ConsulEndpoint.class));
}
@Test
public void consulHealthIndicatorDisabled() {
appContextRunner.withPropertyValues("management.health.consul.enabled=false")
.run(context -> assertThat(context).hasNotFailed()
.doesNotHaveBean(ConsulHealthIndicator.class));
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.cloud.consul;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.catalog.CatalogServicesRequest;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -25,24 +28,47 @@ import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Integration test for {@link ConsulHealthIndicator} when its in the DOWN status.
*
* @author Lomesh Patel (lomeshpatel)
*/
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.cloud.consul.host=invalidhost")
@SpringBootTest
public class ConsulHealthIndicatorDownTest {
@MockBean
private ConsulClient consulClient;
@Autowired
private HealthEndpoint healthEndpoint;
@Test
public void doHealthCheck() {
public void statusIsDownWhenConsulClientFailsToGetLeaderStatus() {
when(consulClient.getStatusLeader()).thenThrow(new RuntimeException("no leader"));
assertThat(this.healthEndpoint.health().getStatus())
.as("health status was not DOWN").isEqualTo(Status.DOWN);
verify(consulClient).getStatusLeader();
}
@Test
public void statusIsDownWhenConsulClientFailsToGetServices() {
Response<String> leaderStatus = new Response<>("OK", 5150L, true,
System.currentTimeMillis());
when(consulClient.getStatusLeader()).thenReturn(leaderStatus);
when(consulClient.getCatalogServices(any(CatalogServicesRequest.class)))
.thenThrow(new RuntimeException("no services"));
assertThat(this.healthEndpoint.health().getStatus())
.as("health status was not DOWN").isEqualTo(Status.DOWN);
verify(consulClient).getCatalogServices(any(CatalogServicesRequest.class));
}
@EnableAutoConfiguration

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2013-2019 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
*
* https://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.consul;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.catalog.CatalogServicesRequest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Integration test for {@link ConsulHealthIndicator} using its lightweight check when its
* in the UP status.
*
* @author Lomesh Patel (lomeshpatel)
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
properties = "spring.cloud.consul.health-indicator.include-services-query=false")
@ContextConfiguration(initializers = ConsulTestcontainers.class)
public class ConsulHealthIndicatorLightweightUpTest {
@SpyBean
private ConsulClient consulClient;
@Autowired
private HealthEndpoint healthEndpoint;
@Test
public void statusIsUp() {
assertThat(this.healthEndpoint.health().getStatus())
.as("health status was not UP").isEqualTo(Status.UP);
verify(consulClient).getStatusLeader();
verify(consulClient, never())
.getCatalogServices(any(CatalogServicesRequest.class));
}
@EnableAutoConfiguration
@SpringBootConfiguration
protected static class TestConfig {
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.cloud.consul;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.catalog.CatalogServicesRequest;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -25,13 +27,18 @@ import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
/**
* Integration test for {@link ConsulHealthIndicator} when its in the UP status.
*
* @author Lomesh Patel (lomeshpatel)
*/
@RunWith(SpringRunner.class)
@@ -39,13 +46,18 @@ import static org.assertj.core.api.Assertions.assertThat;
@ContextConfiguration(initializers = ConsulTestcontainers.class)
public class ConsulHealthIndicatorUpTest {
@SpyBean
private ConsulClient consulClient;
@Autowired
private HealthEndpoint healthEndpoint;
@Test
public void doHealthCheck() {
public void statusIsUp() {
assertThat(this.healthEndpoint.health().getStatus())
.as("health status was not UP").isEqualTo(Status.UP);
verify(consulClient).getStatusLeader();
verify(consulClient).getCatalogServices(any(CatalogServicesRequest.class));
}
@EnableAutoConfiguration