From eb3bf40bdb101e73dd6d575a7b56e141b0b92ccb Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 1 Nov 2021 13:30:00 +0100 Subject: [PATCH] Bring back Elasticsearch RestClient auto-configuration Prior to this commit, Spring Boot would only auto-configure the `RestHighLevelClient` and `RestClientBuilder` if the `RestHighLevelClient` was present. This was done in 1d73d4ed. This commit brings back the exposing of the `RestClient` bean in when exposing the `RestHighLevelClient` or when the `RestHighLevelClient` is not present. It allows for using the auto-configuration and its customizers of the `RestClientBuilder` in a similar way as it is done for the `RestTemplateBuilder` and the `WebClient.Builder`. The presence of the `elasticsearch-rest-high-level-client` module is now optional. This opens the door for potentially adding support for the new Elasticsearch Java Client[1] that is based on the same `RestClient`. The health contributor and its configuration has also been updated to only depend on the low-level RestClient. See gh-28496 [1] https://github.com/elastic/elasticsearch-java --- ...estHealthContributorAutoConfiguration.java | 9 +- ...chRestHealthContributorConfigurations.java | 69 +++++++++ ...althContributorAutoConfigurationTests.java | 1 + ...althContributorAutoConfigurationTests.java | 113 ++++++++++++++ ...lasticsearchRestClientHealthIndicator.java | 85 +++++++++++ .../ElasticsearchRestHealthIndicator.java | 55 +------ ...csearchRestClientHealthIndicatorTests.java | 143 ++++++++++++++++++ ...ElasticsearchRestHealthIndicatorTests.java | 1 + ...sticsearchRestClientAutoConfiguration.java | 3 + ...ElasticsearchRestClientConfigurations.java | 35 ++++- ...ientAutoConfigurationIntegrationTests.java | 49 +++++- ...earchRestClientAutoConfigurationTests.java | 135 ++++++++++++----- .../src/docs/asciidoc/data/nosql.adoc | 10 +- 13 files changed, 604 insertions(+), 104 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorConfigurations.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchRestHealthContributorAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicatorTests.java 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 51e2b1e83f..7beb5a601b 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 @@ -44,14 +44,13 @@ import org.springframework.context.annotation.Bean; @ConditionalOnClass(org.elasticsearch.client.RestHighLevelClient.class) @ConditionalOnBean(org.elasticsearch.client.RestHighLevelClient.class) @ConditionalOnEnabledHealthIndicator("elasticsearch") -public class ElasticSearchRestHealthContributorAutoConfiguration extends - CompositeHealthContributorConfiguration { +public class ElasticSearchRestHealthContributorAutoConfiguration + extends CompositeHealthContributorConfiguration { @Bean @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) - public HealthContributor elasticsearchHealthContributor( - Map clients) { + public HealthContributor elasticsearchHealthContributor(Map clients) { return createContributor(clients); } -} +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorConfigurations.java new file mode 100644 index 0000000000..948a3c2ca3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorConfigurations.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2022 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 org.elasticsearch.client.RestClient; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Elasticsearch rest client health contributor configurations. + * + * @author Filip Hrisafov + */ +class ElasticSearchRestHealthContributorConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(org.elasticsearch.client.RestHighLevelClient.class) + @ConditionalOnBean(org.elasticsearch.client.RestHighLevelClient.class) + @Deprecated + static class RestHighLevelClientHealthContributorConfiguration extends + CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) + HealthContributor elasticsearchHealthContributor( + Map clients) { + return createContributor(clients); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(RestClient.class) + @ConditionalOnMissingBean(org.elasticsearch.client.RestHighLevelClient.class) + static class RestClientHealthContributorConfiguration + extends CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) + HealthContributor elasticsearchHealthContributor(Map clients) { + return createContributor(clients); + } + + } + +} 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 index c14fc96e4f..0c81b6af65 100644 --- 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 @@ -50,6 +50,7 @@ class ElasticsearchReactiveHealthContributorAutoConfigurationTests { } @Test + @SuppressWarnings("deprecation") void runWithRegularIndicatorShouldOnlyCreateReactiveIndicator() { this.contextRunner .withConfiguration(AutoConfigurations.of(ElasticSearchRestHealthContributorAutoConfiguration.class)) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchRestHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchRestHealthContributorAutoConfigurationTests.java new file mode 100644 index 0000000000..0a3418329f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchRestHealthContributorAutoConfigurationTests.java @@ -0,0 +1,113 @@ +/* + * 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.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchRestClientHealthIndicator; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchRestHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ElasticsearchRestClientAutoConfiguration}. + * + * @author Filip Hrisafov + */ +class ElasticsearchRestHealthContributorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, + ElasticSearchRestHealthContributorAutoConfiguration.class, + HealthContributorAutoConfiguration.class)); + + @Test + @SuppressWarnings("deprecation") + void runShouldCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ElasticsearchRestHealthIndicator.class) + .hasBean("elasticsearchHealthContributor")); + } + + @Test + void runWithoutRestHighLevelClientAndWithoutRestClientShouldNotCreateIndicator() { + this.contextRunner + .withClassLoader( + new FilteredClassLoader(org.elasticsearch.client.RestHighLevelClient.class, RestClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ElasticsearchRestClientHealthIndicator.class) + .doesNotHaveBean("elasticsearchHealthContributor")); + } + + @Test + @SuppressWarnings("deprecation") + void runWithoutRestHighLevelClientAndWithRestClientShouldCreateIndicator() { + this.contextRunner.withUserConfiguration(CustomRestClientConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ElasticsearchRestClientHealthIndicator.class) + .doesNotHaveBean(ElasticsearchRestHealthIndicator.class) + .hasBean("elasticsearchHealthContributor")); + } + + @Test + @SuppressWarnings("deprecation") + void runWithRestHighLevelClientAndWithRestClientShouldCreateIndicator() { + this.contextRunner.withUserConfiguration(CustomRestHighClientConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ElasticsearchRestClientHealthIndicator.class) + .hasBean("elasticsearchHealthContributor")); + } + + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withPropertyValues("management.health.elasticsearch.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(ElasticsearchRestClientHealthIndicator.class) + .doesNotHaveBean("elasticsearchHealthContributor")); + } + + @Configuration(proxyBeanMethods = false) + static class CustomRestClientConfiguration { + + @Bean + RestClient customRestClient(RestClientBuilder builder) { + return builder.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("deprecation") + static class CustomRestHighClientConfiguration { + + @Bean + org.elasticsearch.client.RestHighLevelClient customRestHighClient(RestClientBuilder builder) { + return new org.elasticsearch.client.RestHighLevelClient(builder); + } + + @Bean + RestClient customClient(org.elasticsearch.client.RestHighLevelClient restHighLevelClient) { + return restHighLevelClient.getLowLevelClient(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicator.java new file mode 100644 index 0000000000..da0821e143 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicator.java @@ -0,0 +1,85 @@ +/* + * 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.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; + +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.json.JsonParser; +import org.springframework.boot.json.JsonParserFactory; +import org.springframework.util.StreamUtils; + +/** + * {@link HealthIndicator} for an Elasticsearch cluster using a {@link RestClient}. + * + * @author Artsiom Yudovin + * @author Brian Clozel + * @author Filip Hrisafov + * @since 2.7 + */ +public class ElasticsearchRestClientHealthIndicator extends AbstractHealthIndicator { + + private static final String RED_STATUS = "red"; + + private final RestClient client; + + private final JsonParser jsonParser; + + public ElasticsearchRestClientHealthIndicator(RestClient client) { + super("Elasticsearch health check failed"); + this.client = client; + this.jsonParser = JsonParserFactory.getJsonParser(); + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + Response response = this.client.performRequest(new Request("GET", "/_cluster/health/")); + StatusLine statusLine = response.getStatusLine(); + if (statusLine.getStatusCode() != HttpStatus.SC_OK) { + builder.down(); + builder.withDetail("statusCode", statusLine.getStatusCode()); + builder.withDetail("reasonPhrase", statusLine.getReasonPhrase()); + return; + } + try (InputStream inputStream = response.getEntity().getContent()) { + doHealthCheck(builder, StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8)); + } + } + + private void doHealthCheck(Health.Builder builder, String json) { + Map response = this.jsonParser.parseMap(json); + String status = (String) response.get("status"); + if (RED_STATUS.equals(status)) { + builder.outOfService(); + } + else { + builder.up(); + } + builder.withDetails(response); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java index 468f235c3b..596018ed16 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java @@ -16,22 +16,9 @@ package org.springframework.boot.actuate.elasticsearch; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import org.apache.http.HttpStatus; -import org.apache.http.StatusLine; -import org.elasticsearch.client.Request; -import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; -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.json.JsonParser; -import org.springframework.boot.json.JsonParserFactory; -import org.springframework.util.StreamUtils; /** * {@link HealthIndicator} for an Elasticsearch cluster using a {@link RestClient}. @@ -40,14 +27,11 @@ import org.springframework.util.StreamUtils; * @author Brian Clozel * @author Filip Hrisafov * @since 2.1.1 + * @deprecated since 2.7.0 for removal in 2.9.0 in favor of + * {@link ElasticsearchRestClientHealthIndicator} */ -public class ElasticsearchRestHealthIndicator extends AbstractHealthIndicator { - - private static final String RED_STATUS = "red"; - - private final RestClient client; - - private final JsonParser jsonParser; +@Deprecated +public class ElasticsearchRestHealthIndicator extends ElasticsearchRestClientHealthIndicator { @SuppressWarnings("deprecation") public ElasticsearchRestHealthIndicator(org.elasticsearch.client.RestHighLevelClient client) { @@ -55,36 +39,7 @@ public class ElasticsearchRestHealthIndicator extends AbstractHealthIndicator { } public ElasticsearchRestHealthIndicator(RestClient client) { - super("Elasticsearch health check failed"); - this.client = client; - this.jsonParser = JsonParserFactory.getJsonParser(); - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - Response response = this.client.performRequest(new Request("GET", "/_cluster/health/")); - StatusLine statusLine = response.getStatusLine(); - if (statusLine.getStatusCode() != HttpStatus.SC_OK) { - builder.down(); - builder.withDetail("statusCode", statusLine.getStatusCode()); - builder.withDetail("reasonPhrase", statusLine.getReasonPhrase()); - return; - } - try (InputStream inputStream = response.getEntity().getContent()) { - doHealthCheck(builder, StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8)); - } - } - - private void doHealthCheck(Health.Builder builder, String json) { - Map response = this.jsonParser.parseMap(json); - String status = (String) response.get("status"); - if (RED_STATUS.equals(status)) { - builder.outOfService(); - } - else { - builder.up(); - } - builder.withDetails(response); + super(client); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicatorTests.java new file mode 100644 index 0000000000..80550160b0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestClientHealthIndicatorTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-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.boot.actuate.elasticsearch; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Map; + +import org.apache.http.StatusLine; +import org.apache.http.entity.BasicHttpEntity; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ElasticsearchRestClientHealthIndicator}. + * + * @author Artsiom Yudovin + * @author Filip Hrisafov + */ +class ElasticsearchRestClientHealthIndicatorTests { + + private final RestClient restClient = mock(RestClient.class); + + private final ElasticsearchRestClientHealthIndicator elasticsearchRestHealthIndicator = new ElasticsearchRestClientHealthIndicator( + this.restClient); + + @Test + void elasticsearchIsUp() throws IOException { + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(createJsonResult(200, "green").getBytes())); + Response response = mock(Response.class); + StatusLine statusLine = mock(StatusLine.class); + given(statusLine.getStatusCode()).willReturn(200); + given(response.getStatusLine()).willReturn(statusLine); + given(response.getEntity()).willReturn(httpEntity); + given(this.restClient.performRequest(any(Request.class))).willReturn(response); + Health health = this.elasticsearchRestHealthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertHealthDetailsWithStatus(health.getDetails(), "green"); + } + + @Test + void elasticsearchWithYellowStatusIsUp() throws IOException { + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(createJsonResult(200, "yellow").getBytes())); + Response response = mock(Response.class); + StatusLine statusLine = mock(StatusLine.class); + given(statusLine.getStatusCode()).willReturn(200); + given(response.getStatusLine()).willReturn(statusLine); + given(response.getEntity()).willReturn(httpEntity); + given(this.restClient.performRequest(any(Request.class))).willReturn(response); + Health health = this.elasticsearchRestHealthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertHealthDetailsWithStatus(health.getDetails(), "yellow"); + } + + @Test + void elasticsearchIsDown() throws IOException { + given(this.restClient.performRequest(any(Request.class))).willThrow(new IOException("Couldn't connect")); + Health health = this.elasticsearchRestHealthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).contains(entry("error", "java.io.IOException: Couldn't connect")); + } + + @Test + void elasticsearchIsDownByResponseCode() throws IOException { + Response response = mock(Response.class); + StatusLine statusLine = mock(StatusLine.class); + given(statusLine.getStatusCode()).willReturn(500); + given(statusLine.getReasonPhrase()).willReturn("Internal server error"); + given(response.getStatusLine()).willReturn(statusLine); + given(this.restClient.performRequest(any(Request.class))).willReturn(response); + Health health = this.elasticsearchRestHealthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).contains(entry("statusCode", 500), + entry("reasonPhrase", "Internal server error")); + } + + @Test + void elasticsearchIsOutOfServiceByStatus() throws IOException { + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContent(new ByteArrayInputStream(createJsonResult(200, "red").getBytes())); + Response response = mock(Response.class); + StatusLine statusLine = mock(StatusLine.class); + given(statusLine.getStatusCode()).willReturn(200); + given(response.getStatusLine()).willReturn(statusLine); + given(response.getEntity()).willReturn(httpEntity); + given(this.restClient.performRequest(any(Request.class))).willReturn(response); + Health health = this.elasticsearchRestHealthIndicator.health(); + 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 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}"; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicatorTests.java index c283c049e2..8312fb5dbb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicatorTests.java @@ -42,6 +42,7 @@ import static org.mockito.Mockito.mock; * @author Artsiom Yudovin * @author Filip Hrisafov */ +@Deprecated class ElasticsearchRestHealthIndicatorTests { private final RestClient restClient = mock(RestClient.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java index d495db352f..da462db9f2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java @@ -22,7 +22,9 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientBuilderConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientConfiguration; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientSnifferConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientWithRestHighLevelClientConfiguration; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestHighLevelClientConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Import; @@ -40,6 +42,7 @@ import org.springframework.context.annotation.Import; @EnableConfigurationProperties({ ElasticsearchProperties.class, ElasticsearchRestClientProperties.class, DeprecatedElasticsearchRestClientProperties.class }) @Import({ RestClientBuilderConfiguration.class, RestHighLevelClientConfiguration.class, + RestClientWithRestHighLevelClientConfiguration.class, RestClientConfiguration.class, RestClientSnifferConfiguration.class }) public class ElasticsearchRestClientAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java index ceea098c85..d8ed7df417 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java @@ -36,6 +36,7 @@ import org.elasticsearch.client.sniff.SnifferBuilder; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; @@ -46,6 +47,7 @@ import org.springframework.util.StringUtils; * Elasticsearch rest client configurations. * * @author Stephane Nicoll + * @author Filip Hrisafov */ class ElasticsearchRestClientConfigurations { @@ -126,16 +128,41 @@ class ElasticsearchRestClientConfigurations { @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Sniffer.class) + @ConditionalOnClass(org.elasticsearch.client.RestHighLevelClient.class) @ConditionalOnSingleCandidate(org.elasticsearch.client.RestHighLevelClient.class) + @ConditionalOnMissingBean(RestClient.class) + static class RestClientWithRestHighLevelClientConfiguration { + + @Bean + RestClient elasticsearchRestClient(org.elasticsearch.client.RestHighLevelClient restHighLevelClient) { + return restHighLevelClient.getLowLevelClient(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.elasticsearch.client.RestHighLevelClient") + @ConditionalOnMissingBean(RestClient.class) + static class RestClientConfiguration { + + @Bean + RestClient elasticsearchRestClient(RestClientBuilder restClientBuilder) { + return restClientBuilder.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Sniffer.class) + @ConditionalOnSingleCandidate(RestClient.class) static class RestClientSnifferConfiguration { @Bean @ConditionalOnMissingBean - Sniffer elasticsearchSniffer(org.elasticsearch.client.RestHighLevelClient client, - ElasticsearchRestClientProperties properties, + @SuppressWarnings("deprecation") + Sniffer elasticsearchSniffer(RestClient client, ElasticsearchRestClientProperties properties, DeprecatedElasticsearchRestClientProperties deprecatedProperties) { - SnifferBuilder builder = Sniffer.builder(client.getLowLevelClient()); + SnifferBuilder builder = Sniffer.builder(client); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); Duration interval = deprecatedProperties.isCustomized() ? deprecatedProperties.getSniffer().getInterval() : properties.getSniffer().getInterval(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java index 24be99bfdc..eb93f9979a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -16,19 +16,26 @@ package org.springframework.boot.autoconfigure.elasticsearch; +import java.io.InputStream; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; import org.junit.jupiter.api.Test; import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.testcontainers.DockerImageNames; @@ -40,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Brian Clozel * @author Vedran Pavic * @author Evgeniy Cheban + * @author Filip Hrisafov */ @Testcontainers(disabledWithoutDocker = true) class ElasticsearchRestClientAutoConfigurationIntegrationTests { @@ -53,7 +61,7 @@ class ElasticsearchRestClientAutoConfigurationIntegrationTests { @Test @SuppressWarnings("deprecation") - void restClientCanQueryElasticsearchNode() { + void restHighLevelClientCanQueryElasticsearchNode() { this.contextRunner .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") @@ -70,4 +78,43 @@ class ElasticsearchRestClientAutoConfigurationIntegrationTests { }); } + @Test + void restClientCanQueryElasticsearchNode() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), + "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") + .run((context) -> { + RestClient client = context.getBean(RestClient.class); + Request index = new Request("PUT", "/test/_doc/2"); + index.setJsonEntity("{" + " \"a\": \"alpha\"," + " \"b\": \"bravo\"" + "}"); + client.performRequest(index); + Request getRequest = new Request("GET", "/test/_doc/2"); + Response response = client.performRequest(getRequest); + try (InputStream input = response.getEntity().getContent()) { + JsonNode result = new ObjectMapper().readTree(input); + assertThat(result.path("found").asBoolean()).isTrue(); + } + }); + } + + @Test + @SuppressWarnings("deprecation") + void restClientCanQueryElasticsearchNodeWithoutHighLevelClient() { + this.contextRunner.withClassLoader(new FilteredClassLoader(org.elasticsearch.client.RestHighLevelClient.class)) + .withPropertyValues("spring.elasticsearch.uris=" + elasticsearch.getHttpHostAddress(), + "spring.elasticsearch.connection-timeout=120s", "spring.elasticsearch.socket-timeout=120s") + .run((context) -> { + RestClient client = context.getBean(RestClient.class); + Request index = new Request("PUT", "/test/_doc/3"); + index.setJsonEntity("{" + " \"a\": \"alpha\"," + " \"b\": \"bravo\"" + "}"); + client.performRequest(index); + Request getRequest = new Request("GET", "/test/_doc/3"); + Response response = client.performRequest(getRequest); + try (InputStream input = response.getEntity().getContent()) { + JsonNode result = new ObjectMapper().readTree(input); + assertThat(result.path("found").asBoolean()).isTrue(); + } + }); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java index d1f3049ebc..6a34c51d50 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java @@ -63,17 +63,20 @@ class ElasticsearchRestClientAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class)); @Test - void configureShouldOnlyCreateHighLevelRestClient() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(RestClient.class) - .hasSingleBean(RestClientBuilder.class) - .hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class)); - + void configureShouldCreateHighLevelAndLowLevelRestClient() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(RestClient.class) + .hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class) + .hasSingleBean(RestClientBuilder.class); + assertThat(context.getBean(RestClient.class)) + .isEqualTo(context.getBean(org.elasticsearch.client.RestHighLevelClient.class).getLowLevelClient()); + }); } @Test - void configureWithoutRestHighLevelClientShouldOnlyCreateRestClientBuilder() { + void configureWithoutRestHighLevelClientShouldOnlyCreateRestClientBuilderAndRestClient() { this.contextRunner.withClassLoader(new FilteredClassLoader(org.elasticsearch.client.RestHighLevelClient.class)) - .run((context) -> assertThat(context).doesNotHaveBean(RestClient.class) + .run((context) -> assertThat(context).hasSingleBean(RestClient.class) .doesNotHaveBean(org.elasticsearch.client.RestHighLevelClient.class) .hasSingleBean(RestClientBuilder.class)); } @@ -87,25 +90,52 @@ class ElasticsearchRestClientAutoConfigurationTests { .hasBean("customRestClient")); } + @Test + void configureWhenCustomRestHighLevelClientIsNotPresent() { + this.contextRunner.withClassLoader(new FilteredClassLoader(org.elasticsearch.client.RestHighLevelClient.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(org.elasticsearch.client.RestHighLevelClient.class) + .hasSingleBean(RestClient.class).hasSingleBean(RestClientBuilder.class)); + } + @Test void configureWhenCustomRestHighLevelClientShouldBackOff() { - this.contextRunner.withUserConfiguration(CustomRestHighLevelClientConfiguration.class).run( - (context) -> assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class)); + this.contextRunner.withUserConfiguration(CustomRestHighLevelClientConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class) + .hasSingleBean(RestClient.class).hasBean("elasticsearchRestClient")); + } + + @Test + void configureWhenCustomRestHighLevelClientAndRestClientWithRestHighLevelClientShouldBackOff() { + this.contextRunner.withUserConfiguration(CustomRestHighLevelClientWithRestClientConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class) + .hasSingleBean(RestClient.class).hasBean("customRestClient")); } @Test void configureWhenDefaultRestClientShouldCreateWhenNoUniqueRestHighLevelClient() { this.contextRunner.withUserConfiguration(TwoCustomRestHighLevelClientConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(RestClient.class); Map restHighLevelClients = context .getBeansOfType(org.elasticsearch.client.RestHighLevelClient.class); assertThat(restHighLevelClients).hasSize(2); }); } + @Test + void configureWhenDefaultRestClientShouldCreateWhenNoUniqueRestClient() { + this.contextRunner.withClassLoader(new FilteredClassLoader(org.elasticsearch.client.RestHighLevelClient.class)) + .withUserConfiguration(TwoCustomRestClientConfiguration.class).run((context) -> { + Map restClients = context.getBeansOfType(RestClient.class); + assertThat(restClients).hasSize(2); + }); + } + @Test void configureWhenBuilderCustomizerShouldApply() { this.contextRunner.withUserConfiguration(BuilderCustomizerConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class); + assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class) + .hasSingleBean(RestClient.class); org.elasticsearch.client.RestHighLevelClient restClient = context .getBean(org.elasticsearch.client.RestHighLevelClient.class); RestClient lowLevelClient = restClient.getLowLevelClient(); @@ -118,9 +148,8 @@ class ElasticsearchRestClientAutoConfigurationTests { @Test void configureWithNoTimeoutsApplyDefaults() { this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class); - org.elasticsearch.client.RestHighLevelClient restClient = context - .getBean(org.elasticsearch.client.RestHighLevelClient.class); + assertThat(context).hasSingleBean(RestClient.class); + RestClient restClient = context.getBean(RestClient.class); assertTimeouts(restClient, Duration.ofMillis(RestClientBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS), Duration.ofMillis(RestClientBuilder.DEFAULT_SOCKET_TIMEOUT_MILLIS)); }); @@ -130,9 +159,8 @@ class ElasticsearchRestClientAutoConfigurationTests { void configureWithLegacyCustomTimeouts() { this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s", "spring.elasticsearch.rest.read-timeout=1m").run((context) -> { - assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class); - org.elasticsearch.client.RestHighLevelClient restClient = context - .getBean(org.elasticsearch.client.RestHighLevelClient.class); + assertThat(context).hasSingleBean(RestClient.class); + RestClient restClient = context.getBean(RestClient.class); assertTimeouts(restClient, Duration.ofSeconds(15), Duration.ofMinutes(1)); }); } @@ -141,25 +169,23 @@ class ElasticsearchRestClientAutoConfigurationTests { void configureWithCustomTimeouts() { this.contextRunner.withPropertyValues("spring.elasticsearch.connection-timeout=15s", "spring.elasticsearch.socket-timeout=1m").run((context) -> { - assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class); - org.elasticsearch.client.RestHighLevelClient restClient = context - .getBean(org.elasticsearch.client.RestHighLevelClient.class); + assertThat(context).hasSingleBean(RestClient.class); + RestClient restClient = context.getBean(RestClient.class); assertTimeouts(restClient, Duration.ofSeconds(15), Duration.ofMinutes(1)); }); } - private static void assertTimeouts(org.elasticsearch.client.RestHighLevelClient restClient, Duration connectTimeout, - Duration readTimeout) { - assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.socketTimeout") + private static void assertTimeouts(RestClient restClient, Duration connectTimeout, Duration readTimeout) { + assertThat(restClient).extracting("client.defaultConfig.socketTimeout") .isEqualTo(Math.toIntExact(readTimeout.toMillis())); - assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.connectTimeout") + assertThat(restClient).extracting("client.defaultConfig.connectTimeout") .isEqualTo(Math.toIntExact(connectTimeout.toMillis())); } @ParameterizedPropertyPrefixTest void configureUriWithNoScheme(String prefix) { this.contextRunner.withPropertyValues(prefix + "uris=localhost:9876").run((context) -> { - RestClient client = context.getBean(org.elasticsearch.client.RestHighLevelClient.class).getLowLevelClient(); + RestClient client = context.getBean(RestClient.class); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9876"); }); @@ -168,7 +194,7 @@ class ElasticsearchRestClientAutoConfigurationTests { @ParameterizedPropertyPrefixTest void configureUriWithUsernameOnly(String prefix) { this.contextRunner.withPropertyValues(prefix + "uris=http://user@localhost:9200").run((context) -> { - RestClient client = context.getBean(org.elasticsearch.client.RestHighLevelClient.class).getLowLevelClient(); + RestClient client = context.getBean(RestClient.class); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9200"); assertThat(client) @@ -184,7 +210,7 @@ class ElasticsearchRestClientAutoConfigurationTests { @ParameterizedPropertyPrefixTest void configureUriWithUsernameAndEmptyPassword(String prefix) { this.contextRunner.withPropertyValues(prefix + "uris=http://user:@localhost:9200").run((context) -> { - RestClient client = context.getBean(org.elasticsearch.client.RestHighLevelClient.class).getLowLevelClient(); + RestClient client = context.getBean(RestClient.class); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9200"); assertThat(client) @@ -201,8 +227,7 @@ class ElasticsearchRestClientAutoConfigurationTests { void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet(String prefix) { this.contextRunner.withPropertyValues(prefix + "uris=http://user:password@localhost:9200,localhost:9201", prefix + "username=admin", prefix + "password=admin").run((context) -> { - RestClient client = context.getBean(org.elasticsearch.client.RestHighLevelClient.class) - .getLowLevelClient(); + RestClient client = context.getBean(RestClient.class); assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) .containsExactly("http://localhost:9200", "http://localhost:9201"); assertThat(client) @@ -224,7 +249,7 @@ class ElasticsearchRestClientAutoConfigurationTests { @Test void configureWithCustomPathPrefix() { this.contextRunner.withPropertyValues("spring.elasticsearch.path-prefix=/some/prefix").run((context) -> { - RestClient client = context.getBean(org.elasticsearch.client.RestHighLevelClient.class).getLowLevelClient(); + RestClient client = context.getBean(RestClient.class); assertThat(client).extracting("pathPrefix").isEqualTo("/some/prefix"); }); } @@ -233,19 +258,21 @@ class ElasticsearchRestClientAutoConfigurationTests { void configureWithoutSnifferLibraryShouldNotCreateSniffer() { this.contextRunner.withClassLoader(new FilteredClassLoader("org.elasticsearch.client.sniff")) .run((context) -> assertThat(context).hasSingleBean(org.elasticsearch.client.RestHighLevelClient.class) - .doesNotHaveBean(Sniffer.class)); + .hasSingleBean(RestClient.class).doesNotHaveBean(Sniffer.class)); } @Test - void configureShouldCreateSnifferUsingRestHighLevelClient() { - this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(Sniffer.class); - assertThat(context.getBean(Sniffer.class)).hasFieldOrPropertyWithValue("restClient", - context.getBean(org.elasticsearch.client.RestHighLevelClient.class).getLowLevelClient()); - // Validate shutdown order as the sniffer must be shutdown before the client - assertThat(context.getBeanFactory().getDependentBeans("elasticsearchRestHighLevelClient")) - .contains("elasticsearchSniffer"); - }); + void configureShouldCreateSnifferUsingRestClient() { + this.contextRunner.withClassLoader(new FilteredClassLoader(org.elasticsearch.client.RestHighLevelClient.class)) + .run((context) -> { + assertThat(context).hasSingleBean(Sniffer.class); + assertThat(context.getBean(Sniffer.class)).hasFieldOrPropertyWithValue("restClient", + context.getBean(RestClient.class)); + // Validate shutdown order as the sniffer must be shutdown before the + // client + assertThat(context.getBeanFactory().getDependentBeans("elasticsearchRestClient")) + .contains("elasticsearchSniffer"); + }); } @ParameterizedSnifferPropertyPrefixTest @@ -309,6 +336,21 @@ class ElasticsearchRestClientAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomRestHighLevelClientWithRestClientConfiguration { + + @Bean + org.elasticsearch.client.RestHighLevelClient customRestHighLevelClient(RestClientBuilder builder) { + return new org.elasticsearch.client.RestHighLevelClient(builder); + } + + @Bean + RestClient customRestClient(org.elasticsearch.client.RestHighLevelClient restHighLevelClient) { + return restHighLevelClient.getLowLevelClient(); + } + + } + @Configuration(proxyBeanMethods = false) static class TwoCustomRestHighLevelClientConfiguration { @@ -334,6 +376,21 @@ class ElasticsearchRestClientAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class TwoCustomRestClientConfiguration { + + @Bean + RestClient customRestClient(RestClientBuilder builder) { + return builder.build(); + } + + @Bean + RestClient customRestClient1(RestClientBuilder builder) { + return builder.build(); + } + + } + @ParameterizedTest @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index 56823d5325..9cc927f2bb 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -269,14 +269,14 @@ You can use `spring.elasticsearch.*` properties to further tune how the clients ---- [[data.nosql.elasticsearch.connecting-using-rest.restclient]] -===== Connecting to Elasticsearch using RestHighLevelClient -If you have `elasticsearch-rest-high-level-client` on the classpath, Spring Boot will auto-configure and register a `RestHighLevelClient` bean. -In addition to the properties described previously, to fine-tune the `RestHighLevelClient`, you can register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. +===== Connecting to Elasticsearch using RestClient +If you have `elasticsearch-rest-client` on the classpath, Spring Boot will auto-configure and register a `RestClient` bean. +In addition to the properties described previously, to fine-tune the `RestClient`, you can register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. To take full control over its registration, define a `RestClientBuilder` bean. -TIP: If your application needs access to a "Low Level" `RestClient`, you can get it by calling `client.getLowLevelClient()` on the auto-configured `RestHighLevelClient`. +NOTE: If you have `elasticsearch-rest-high-level-client` on the classpath a `RestHighLevelClient` bean will be exposed as well -Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them on the `RestHighLevelClient` bean. +Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them on the `RestClient` bean. You can further tune how `Sniffer` is configured, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks]