Skip to content

Commit 0a4ba49

Browse files
ayudovinbclozel
authored andcommitted
Add Actuator health checks for Elasticsearch REST clients
This commit adds `ElasticsearchRestHealthIndicator`, a new `HealthIndicator` for Elasticsearch, using the Elasticsearch "low level rest client" provided by the `"org.elasticsearch.client:elasticsearch-rest-client"` dependency. Note that Spring Boot will auto-configure both low and high level REST clients, but since the high level one is using the former, a single health indicator will cover both cases. See gh-15211
1 parent 6a766cf commit 0a4ba49

File tree

5 files changed

+283
-2
lines changed

5 files changed

+283
-2
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@
265265
<optional>true</optional>
266266
</dependency>
267267
<dependency>
268-
<groupId>org.elasticsearch</groupId>
269-
<artifactId>elasticsearch</artifactId>
268+
<groupId>org.elasticsearch.client</groupId>
269+
<artifactId>elasticsearch-rest-client</artifactId>
270270
<optional>true</optional>
271271
</dependency>
272272
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.elasticsearch;
18+
19+
import java.util.Map;
20+
21+
import org.elasticsearch.client.RestClient;
22+
23+
import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthIndicatorConfiguration;
24+
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
25+
import org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration;
26+
import org.springframework.boot.actuate.elasticsearch.ElasticsearchRestHealthIndicator;
27+
import org.springframework.boot.actuate.health.HealthIndicator;
28+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
29+
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
30+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
33+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
34+
import org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration;
35+
import org.springframework.context.annotation.Bean;
36+
import org.springframework.context.annotation.Configuration;
37+
38+
/**
39+
* {@link EnableAutoConfiguration Auto-configuration} for
40+
* {@link ElasticsearchRestHealthIndicator} using the {@link RestClient}.
41+
*
42+
* @author Artsiom Yudovin
43+
* @since 2.1.0
44+
*/
45+
46+
@Configuration
47+
@ConditionalOnClass(RestClient.class)
48+
@ConditionalOnBean(RestClient.class)
49+
@ConditionalOnEnabledHealthIndicator("elasticsearch")
50+
@AutoConfigureBefore(HealthIndicatorAutoConfiguration.class)
51+
@AutoConfigureAfter({ RestClientAutoConfiguration.class,
52+
ElasticSearchClientHealthIndicatorAutoConfiguration.class })
53+
public class ElasticSearchRestHealthIndicatorAutoConfiguration extends
54+
CompositeHealthIndicatorConfiguration<ElasticsearchRestHealthIndicator, RestClient> {
55+
56+
private final Map<String, RestClient> clients;
57+
58+
public ElasticSearchRestHealthIndicatorAutoConfiguration(
59+
Map<String, RestClient> clients) {
60+
this.clients = clients;
61+
}
62+
63+
@Bean
64+
@ConditionalOnMissingBean(name = "elasticsearchRestHealthIndicator")
65+
public HealthIndicator elasticsearchRestHealthIndicator() {
66+
return createHealthIndicator(this.clients);
67+
}
68+
69+
@Override
70+
protected ElasticsearchRestHealthIndicator createHealthIndicator(RestClient client) {
71+
return new ElasticsearchRestHealthIndicator(client);
72+
}
73+
74+
}

spring-boot-project/spring-boot-actuator/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@
7777
<artifactId>jest</artifactId>
7878
<optional>true</optional>
7979
</dependency>
80+
<dependency>
81+
<groupId>org.elasticsearch.client</groupId>
82+
<artifactId>elasticsearch-rest-client</artifactId>
83+
<optional>true</optional>
84+
</dependency>
8085
<dependency>
8186
<groupId>io.undertow</groupId>
8287
<artifactId>undertow-servlet</artifactId>
@@ -338,5 +343,6 @@
338343
<artifactId>jsonassert</artifactId>
339344
<scope>test</scope>
340345
</dependency>
346+
341347
</dependencies>
342348
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.elasticsearch;
18+
19+
import java.io.InputStreamReader;
20+
import java.nio.charset.StandardCharsets;
21+
22+
import com.google.gson.JsonElement;
23+
import com.google.gson.JsonParser;
24+
import org.apache.http.HttpStatus;
25+
import org.elasticsearch.client.Request;
26+
import org.elasticsearch.client.Response;
27+
import org.elasticsearch.client.RestClient;
28+
29+
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
30+
import org.springframework.boot.actuate.health.Health;
31+
import org.springframework.boot.actuate.health.HealthIndicator;
32+
33+
/**
34+
* {@link HealthIndicator} for an Elasticsearch cluster by REST.
35+
*
36+
* @author Artsiom Yudovin
37+
* @since 2.1.0
38+
*/
39+
public class ElasticsearchRestHealthIndicator extends AbstractHealthIndicator {
40+
41+
private final RestClient client;
42+
43+
private final JsonParser jsonParser = new JsonParser();
44+
45+
public ElasticsearchRestHealthIndicator(RestClient client) {
46+
super("Elasticsearch health check failed");
47+
this.client = client;
48+
}
49+
50+
@Override
51+
protected void doHealthCheck(Health.Builder builder) throws Exception {
52+
Response response = this.client
53+
.performRequest(new Request("GET", "/_cluster/health/"));
54+
55+
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
56+
builder.down();
57+
}
58+
else {
59+
try (InputStreamReader reader = new InputStreamReader(
60+
response.getEntity().getContent(), StandardCharsets.UTF_8)) {
61+
JsonElement root = this.jsonParser.parse(reader);
62+
JsonElement status = root.getAsJsonObject().get("status");
63+
if (status.getAsString()
64+
.equals(io.searchbox.cluster.Health.Status.RED.getKey())) {
65+
builder.outOfService();
66+
}
67+
else {
68+
builder.up();
69+
}
70+
}
71+
}
72+
}
73+
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.elasticsearch;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.IOException;
21+
22+
import org.apache.http.StatusLine;
23+
import org.apache.http.entity.BasicHttpEntity;
24+
import org.elasticsearch.client.Request;
25+
import org.elasticsearch.client.Response;
26+
import org.elasticsearch.client.RestClient;
27+
import org.junit.Test;
28+
29+
import org.springframework.boot.actuate.health.Status;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.mockito.ArgumentMatchers.any;
33+
import static org.mockito.BDDMockito.mock;
34+
import static org.mockito.BDDMockito.when;
35+
36+
/**
37+
* Tests for {@link ElasticsearchRestHealthIndicator}.
38+
*
39+
* @author Artsiom Yudovin
40+
*/
41+
public class ElasticsearchRestHealthIndicatorTest {
42+
43+
private final RestClient restClient = mock(RestClient.class);
44+
45+
private final ElasticsearchRestHealthIndicator elasticsearchRestHealthIndicator = new ElasticsearchRestHealthIndicator(
46+
this.restClient);
47+
48+
@Test
49+
public void elasticsearchIsUp() throws IOException {
50+
BasicHttpEntity httpEntity = new BasicHttpEntity();
51+
httpEntity.setContent(
52+
new ByteArrayInputStream(createJsonResult(200, "green").getBytes()));
53+
54+
Response response = mock(Response.class);
55+
StatusLine statusLine = mock(StatusLine.class);
56+
57+
when(statusLine.getStatusCode()).thenReturn(200);
58+
when(response.getStatusLine()).thenReturn(statusLine);
59+
when(response.getEntity()).thenReturn(httpEntity);
60+
when(this.restClient.performRequest(any(Request.class))).thenReturn(response);
61+
62+
assertThat(this.elasticsearchRestHealthIndicator.health().getStatus())
63+
.isEqualTo(Status.UP);
64+
}
65+
66+
@Test
67+
public void elasticsearchIsDown() throws IOException {
68+
when(this.restClient.performRequest(any(Request.class)))
69+
.thenThrow(new IOException("Couldn't connect"));
70+
71+
assertThat(this.elasticsearchRestHealthIndicator.health().getStatus())
72+
.isEqualTo(Status.DOWN);
73+
}
74+
75+
@Test
76+
public void elasticsearchIsDownByResponseCode() throws IOException {
77+
78+
Response response = mock(Response.class);
79+
StatusLine statusLine = mock(StatusLine.class);
80+
81+
when(statusLine.getStatusCode()).thenReturn(500);
82+
when(response.getStatusLine()).thenReturn(statusLine);
83+
when(this.restClient.performRequest(any(Request.class))).thenReturn(response);
84+
85+
assertThat(this.elasticsearchRestHealthIndicator.health().getStatus())
86+
.isEqualTo(Status.DOWN);
87+
}
88+
89+
@Test
90+
public void elasticsearchIsOutOfServiceByStatus() throws IOException {
91+
BasicHttpEntity httpEntity = new BasicHttpEntity();
92+
httpEntity.setContent(
93+
new ByteArrayInputStream(createJsonResult(200, "red").getBytes()));
94+
95+
Response response = mock(Response.class);
96+
StatusLine statusLine = mock(StatusLine.class);
97+
98+
when(statusLine.getStatusCode()).thenReturn(200);
99+
when(response.getStatusLine()).thenReturn(statusLine);
100+
when(response.getEntity()).thenReturn(httpEntity);
101+
when(this.restClient.performRequest(any(Request.class))).thenReturn(response);
102+
103+
assertThat(this.elasticsearchRestHealthIndicator.health().getStatus())
104+
.isEqualTo(Status.OUT_OF_SERVICE);
105+
}
106+
107+
private String createJsonResult(int responseCode, String status) {
108+
String json;
109+
if (responseCode == 200) {
110+
json = String.format("{\"cluster_name\":\"elasticsearch\","
111+
+ "\"status\":\"%s\",\"timed_out\":false,\"number_of_nodes\":1,"
112+
+ "\"number_of_data_nodes\":1,\"active_primary_shards\":0,"
113+
+ "\"active_shards\":0,\"relocating_shards\":0,\"initializing_shards\":0,"
114+
+ "\"unassigned_shards\":0,\"delayed_unassigned_shards\":0,"
115+
+ "\"number_of_pending_tasks\":0,\"number_of_in_flight_fetch\":0,"
116+
+ "\"task_max_waiting_in_queue_millis\":0,\"active_shards_percent_as_number\":100.0}",
117+
status);
118+
}
119+
else {
120+
json = "{\n" + " \"error\": \"Server Error\",\n" + " \"status\": "
121+
+ responseCode + "\n" + "}";
122+
}
123+
124+
return json;
125+
}
126+
127+
}

0 commit comments

Comments
 (0)