From 203878a16f71d03cc88c3e602740fda75c202e59 Mon Sep 17 00:00:00 2001 From: Aleksander Lech Date: Mon, 20 Apr 2020 18:30:04 +0400 Subject: [PATCH 1/2] Add support for reactive Elasticsearch healthcheck Prior to this commit, configuring a reactive Elasticsearch client would auto-configure an Actuator Health check using a synchronous client, with the default configuration properties (so tarting localhost:9200). This would lead to false reports of unhealthy Elasticsearch clusters when using reactive clients. This commit reproduces the logic for MongoDB repositories: if a reactive variant is available, it is selected for the health check infrastructure. See gh-21042 --- .../build.gradle | 1 + ...iveHealthContributorAutoConfiguration.java | 59 ++++++++++++++++ ...estHealthContributorAutoConfiguration.java | 4 +- .../main/resources/META-INF/spring.factories | 1 + ...althContributorAutoConfigurationTests.java | 67 +++++++++++++++++++ .../spring-boot-actuator/build.gradle | 1 + .../ElasticsearchReactiveHealthIndicator.java | 61 +++++++++++++++++ 7 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index f49bc9fe02..d64146863c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -97,6 +97,7 @@ dependencies { optional("org.springframework.data:spring-data-mongodb") optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") + optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-solr") optional("org.springframework.integration:spring-integration-core") optional("org.springframework.kafka:spring-kafka") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java new file mode 100644 index 0000000000..5d998fb9e7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 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.boot.actuate.autoconfigure.elasticsearch; + +import java.util.Map; + +import reactor.core.publisher.Flux; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchReactiveHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link ElasticsearchReactiveHealthIndicator} using the + * {@link ReactiveElasticsearchClient}. + * + * @author Aleksander Lech + * @since 2.3 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ ReactiveElasticsearchClient.class, Flux.class }) +@ConditionalOnBean(ReactiveElasticsearchClient.class) +@ConditionalOnEnabledHealthIndicator("elasticsearch") +@AutoConfigureAfter(ReactiveElasticsearchRestClientAutoConfiguration.class) +public class ElasticSearchReactiveHealthContributorAutoConfiguration extends + CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) + public ReactiveHealthContributor elasticsearchHealthContributor(Map clients) { + return createContributor(clients); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java index 5e7787053a..c69b9b0e43 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java @@ -49,8 +49,8 @@ public class ElasticSearchRestHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { @Bean - @ConditionalOnMissingBean(name = { "elasticsearchRestHealthIndicator", "elasticsearchRestHealthContributor" }) - public HealthContributor elasticsearchRestHealthContributor(Map clients) { + @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) + public HealthContributor elasticsearchHealthContributor(Map clients) { return createContributor(clients); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index e297520518..9892020d9a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -15,6 +15,7 @@ org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfi org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthContributorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java new file mode 100644 index 0000000000..391bcda780 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 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.boot.actuate.autoconfigure.elasticsearch; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchReactiveHealthIndicator; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchRestHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ElasticSearchReactiveHealthContributorAutoConfiguration}. + * + * @author Aleksander Lech + */ +class ElasticsearchReactiveHealthContributorAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations + .of(ElasticsearchDataAutoConfiguration.class, ReactiveElasticsearchRestClientAutoConfiguration.class, + ElasticsearchRestClientAutoConfiguration.class, + ElasticSearchReactiveHealthContributorAutoConfiguration.class, + HealthContributorAutoConfiguration.class)); + + @Test + void runShouldCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context) + .hasSingleBean(ElasticsearchReactiveHealthIndicator.class).hasBean("elasticsearchHealthContributor")); + } + + @Test + void runWithRegularIndicatorShouldOnlyCreateReactiveIndicator() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(ElasticSearchRestHealthContributorAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(ElasticsearchReactiveHealthIndicator.class) + .hasBean("elasticsearchHealthContributor") + .doesNotHaveBean(ElasticsearchRestHealthIndicator.class)); + } + + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withPropertyValues("management.health.elasticsearch.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(ElasticsearchReactiveHealthIndicator.class) + .doesNotHaveBean("elasticsearchHealthContributor")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 9afa06c00c..d049d02d97 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -57,6 +57,7 @@ dependencies { optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-rest-webmvc") + optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-solr") optional("org.springframework.integration:spring-integration-core") optional("org.springframework.security:spring-security-core") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java new file mode 100644 index 0000000000..10927dff5d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 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.boot.actuate.elasticsearch; + +import java.util.stream.Collectors; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; + +/** + * {@link HealthIndicator} for an Elasticsearch cluster using a + * {@link ReactiveElasticsearchClient}. + * + * @author Aleksander Lech + * @since 2.3 + */ +public class ElasticsearchReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + private final ReactiveElasticsearchClient client; + + public ElasticsearchReactiveHealthIndicator(ReactiveElasticsearchClient client) { + super("Elasticsearch health check failed"); + this.client = client; + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + return this.client.status().map((status) -> { + if (status.isOk()) { + builder.up(); + } + else { + builder.down(); + } + + builder.withDetails(status.hosts().stream().collect(Collectors + .toMap((host) -> host.getEndpoint().getHostString(), (host) -> host.getState().toString()))); + + return builder.build(); + }); + } + +} From 86d8366ee274fed95f85fbb1015149376aceabb4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 3 Jul 2020 15:09:38 +0200 Subject: [PATCH 2/2] Polish support for reactive Elasticsearch healthcheck Fixes gh-21042 --- ...iveHealthContributorAutoConfiguration.java | 2 +- .../main/resources/META-INF/spring.factories | 2 +- .../spring-boot-actuator/build.gradle | 3 +- .../ElasticsearchReactiveHealthIndicator.java | 46 ++++-- ...ticsearchReactiveHealthIndicatorTests.java | 147 ++++++++++++++++++ 5 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java index 5d998fb9e7..91ebf36ef3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java @@ -40,7 +40,7 @@ import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsea * {@link ReactiveElasticsearchClient}. * * @author Aleksander Lech - * @since 2.3 + * @since 2.3.2 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ReactiveElasticsearchClient.class, Flux.class }) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 9892020d9a..5327c6df9b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -14,8 +14,8 @@ org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationP org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseReactiveHealthContributorAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchReactiveHealthContributorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index d049d02d97..609e5648ad 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -52,12 +52,12 @@ dependencies { optional("org.springframework.amqp:spring-rabbit") optional("org.springframework.data:spring-data-cassandra") optional("org.springframework.data:spring-data-couchbase") + optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-ldap") optional("org.springframework.data:spring-data-mongodb") optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-rest-webmvc") - optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-solr") optional("org.springframework.integration:spring-integration-core") optional("org.springframework.security:spring-security-core") @@ -80,6 +80,7 @@ dependencies { testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.springframework:spring-test") + testImplementation("com.squareup.okhttp3:mockwebserver") testRuntimeOnly("io.projectreactor.netty:reactor-netty") testRuntimeOnly("javax.xml.bind:jaxb-api") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java index 10927dff5d..1c4272c8e0 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java @@ -16,24 +16,31 @@ package org.springframework.boot.actuate.elasticsearch; -import java.util.stream.Collectors; +import java.util.Map; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; /** * {@link HealthIndicator} for an Elasticsearch cluster using a * {@link ReactiveElasticsearchClient}. * + * @author Brian Clozel * @author Aleksander Lech - * @since 2.3 + * @since 2.3.2 */ public class ElasticsearchReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { + }; + + private static final String RED_STATUS = "red"; + private final ReactiveElasticsearchClient client; public ElasticsearchReactiveHealthIndicator(ReactiveElasticsearchClient client) { @@ -43,19 +50,28 @@ public class ElasticsearchReactiveHealthIndicator extends AbstractReactiveHealth @Override protected Mono doHealthCheck(Health.Builder builder) { - return this.client.status().map((status) -> { - if (status.isOk()) { - builder.up(); - } - else { - builder.down(); - } - - builder.withDetails(status.hosts().stream().collect(Collectors - .toMap((host) -> host.getEndpoint().getHostString(), (host) -> host.getState().toString()))); - - return builder.build(); - }); + return this.client.execute((callback) -> callback.get().uri("/_cluster/health/").exchange()) + .flatMap((response) -> { + if (response.statusCode().is2xxSuccessful()) { + return response.bodyToMono(STRING_OBJECT_MAP).map((body) -> { + String status = (String) body.get("status"); + if (RED_STATUS.equals(status)) { + builder.outOfService(); + } + else { + builder.up(); + } + builder.withDetails(body); + return builder.build(); + }); + } + else { + builder.down(); + builder.withDetail("statusCode", response.rawStatusCode()); + builder.withDetail("reasonPhrase", response.statusCode().getReasonPhrase()); + return response.releaseBody().thenReturn(builder.build()); + } + }); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java new file mode 100644 index 0000000000..00e3b65296 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2020 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.boot.actuate.elasticsearch; + +import java.util.Map; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.reactive.DefaultReactiveElasticsearchClient; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ElasticsearchReactiveHealthIndicator} + * + * @author Brian Clozel + */ +class ElasticsearchReactiveHealthIndicatorTests { + + private MockWebServer server; + + private WebClient.Builder builder; + + private ElasticsearchReactiveHealthIndicator healthIndicator; + + @BeforeEach + void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.builder = WebClient.builder().baseUrl(this.server.url("/").toString()); + ReactiveElasticsearchClient client = DefaultReactiveElasticsearchClient + .create(ClientConfiguration.create(this.server.getHostName() + ":" + this.server.getPort())); + this.healthIndicator = new ElasticsearchReactiveHealthIndicator(client); + } + + @AfterEach + void shutdown() throws Exception { + this.server.shutdown(); + } + + @Test + void elasticsearchIsUp() { + setupMockResponse(200, "green"); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertHealthDetailsWithStatus(health.getDetails(), "green"); + } + + @Test + void elasticsearchWithYellowStatusIsUp() { + setupMockResponse(200, "yellow"); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertHealthDetailsWithStatus(health.getDetails(), "yellow"); + } + + @Test + void elasticsearchIsDown() throws Exception { + this.server.shutdown(); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("error")).asString() + .contains("org.springframework.data.elasticsearch.client.NoReachableHostException"); + } + + @Test + void elasticsearchIsDownByResponseCode() { + // first enqueue an OK response since the HostChecker first sends a HEAD request + // to "/" + this.server.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value())); + this.server.enqueue(new MockResponse().setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("statusCode")).asString().isEqualTo("500"); + assertThat(health.getDetails().get("reasonPhrase")).asString().isEqualTo("Internal Server Error"); + } + + @Test + void elasticsearchIsOutOfServiceByStatus() { + setupMockResponse(200, "red"); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE); + assertHealthDetailsWithStatus(health.getDetails(), "red"); + } + + private void assertHealthDetailsWithStatus(Map details, String status) { + assertThat(details).contains(entry("cluster_name", "elasticsearch"), entry("status", status), + entry("timed_out", false), entry("number_of_nodes", 1), entry("number_of_data_nodes", 1), + entry("active_primary_shards", 0), entry("active_shards", 0), entry("relocating_shards", 0), + entry("initializing_shards", 0), entry("unassigned_shards", 0), entry("delayed_unassigned_shards", 0), + entry("number_of_pending_tasks", 0), entry("number_of_in_flight_fetch", 0), + entry("task_max_waiting_in_queue_millis", 0), entry("active_shards_percent_as_number", 100.0)); + } + + private void setupMockResponse(int responseCode, String status) { + // first enqueue an OK response since the HostChecker first sends a HEAD request + // to "/" + this.server.enqueue(new MockResponse()); + MockResponse mockResponse = new MockResponse().setResponseCode(HttpStatus.valueOf(responseCode).value()) + .setBody(createJsonResult(responseCode, status)) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + } + + private String createJsonResult(int responseCode, String status) { + if (responseCode == 200) { + return String.format( + "{\"cluster_name\":\"elasticsearch\"," + + "\"status\":\"%s\",\"timed_out\":false,\"number_of_nodes\":1," + + "\"number_of_data_nodes\":1,\"active_primary_shards\":0," + + "\"active_shards\":0,\"relocating_shards\":0,\"initializing_shards\":0," + + "\"unassigned_shards\":0,\"delayed_unassigned_shards\":0," + + "\"number_of_pending_tasks\":0,\"number_of_in_flight_fetch\":0," + + "\"task_max_waiting_in_queue_millis\":0,\"active_shards_percent_as_number\":100.0}", + status); + } + return "{\n \"error\": \"Server Error\",\n \"status\": " + responseCode + "\n}"; + } + +}