Add Jest-based health indicator

This commit adds a Jest-based health indicator for ElasticSearch. If both
Jest and the Spring Data are available, the latter takes precedence as it
provides more information.

Closes gh-3178
This commit is contained in:
Stephane Nicoll 2016-06-13 14:38:34 +02:00
parent 7afe1d16a6
commit 64bbefd3ac
7 changed files with 351 additions and 85 deletions

View File

@ -46,6 +46,11 @@
<artifactId>jackson-dataformat-xml</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>

View File

@ -0,0 +1,69 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.core.ResolvableType;
/**
* Base class for configurations that can combine source beans using a
* {@link CompositeHealthIndicator}.
*
* @param <H> The health indicator type
* @param <S> T he bean source type
* @since 1.4.0
*/
public abstract class CompositeHealthIndicatorConfiguration<H extends HealthIndicator, S> {
@Autowired
private HealthAggregator healthAggregator;
protected HealthIndicator createHealthIndicator(Map<String, S> beans) {
if (beans.size() == 1) {
return createHealthIndicator(beans.values().iterator().next());
}
CompositeHealthIndicator composite = new CompositeHealthIndicator(
this.healthAggregator);
for (Map.Entry<String, S> entry : beans.entrySet()) {
composite.addHealthIndicator(entry.getKey(),
createHealthIndicator(entry.getValue()));
}
return composite;
}
@SuppressWarnings("unchecked")
protected H createHealthIndicator(S source) {
Class<?>[] generics = ResolvableType
.forClass(CompositeHealthIndicatorConfiguration.class, getClass())
.resolveGenerics();
Class<H> indicatorClass = (Class<H>) generics[0];
Class<S> sourceClass = (Class<S>) generics[1];
try {
return indicatorClass.getConstructor(sourceClass).newInstance(source);
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create indicator "
+ indicatorClass + " for source " + sourceClass, ex);
}
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure;
import java.util.Map;
import io.searchbox.client.JestClient;
import org.elasticsearch.client.Client;
import org.springframework.boot.actuate.health.ElasticsearchHealthIndicator;
import org.springframework.boot.actuate.health.ElasticsearchHealthIndicatorProperties;
import org.springframework.boot.actuate.health.ElasticsearchJestHealthIndicator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Actual ElasticSearch health indicator configurations imported by
* {@link HealthIndicatorAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class ElasticsearchHealthIndicatorConfiguration {
@Configuration
@ConditionalOnBean(Client.class)
@ConditionalOnEnabledHealthIndicator("elasticsearch")
@EnableConfigurationProperties(ElasticsearchHealthIndicatorProperties.class)
static class SpringData extends
CompositeHealthIndicatorConfiguration<ElasticsearchHealthIndicator, Client> {
private final Map<String, Client> clients;
private final ElasticsearchHealthIndicatorProperties properties;
SpringData(Map<String, Client> clients,
ElasticsearchHealthIndicatorProperties properties) {
this.clients = clients;
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(name = "elasticsearchHealthIndicator")
public HealthIndicator elasticsearchHealthIndicator() {
return createHealthIndicator(this.clients);
}
@Override
protected ElasticsearchHealthIndicator createHealthIndicator(Client client) {
return new ElasticsearchHealthIndicator(client, this.properties);
}
}
@Configuration
@ConditionalOnBean(JestClient.class)
@ConditionalOnEnabledHealthIndicator("elasticsearch")
static class Jest extends
CompositeHealthIndicatorConfiguration<ElasticsearchJestHealthIndicator, JestClient> {
private final Map<String, JestClient> clients;
Jest(Map<String, JestClient> clients) {
this.clients = clients;
}
@Bean
@ConditionalOnMissingBean(name = "elasticsearchHealthIndicator")
public HealthIndicator elasticsearchHealthIndicator() {
return createHealthIndicator(this.clients);
}
@Override
protected ElasticsearchJestHealthIndicator createHealthIndicator(JestClient client) {
return new ElasticsearchJestHealthIndicator(client);
}
}
}

View File

@ -25,21 +25,16 @@ import javax.sql.DataSource;
import com.couchbase.client.java.Bucket;
import com.datastax.driver.core.Cluster;
import org.apache.solr.client.solrj.SolrClient;
import org.elasticsearch.client.Client;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.ApplicationHealthIndicator;
import org.springframework.boot.actuate.health.CassandraHealthIndicator;
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
import org.springframework.boot.actuate.health.CouchbaseHealthIndicator;
import org.springframework.boot.actuate.health.DataSourceHealthIndicator;
import org.springframework.boot.actuate.health.DiskSpaceHealthIndicator;
import org.springframework.boot.actuate.health.DiskSpaceHealthIndicatorProperties;
import org.springframework.boot.actuate.health.ElasticsearchHealthIndicator;
import org.springframework.boot.actuate.health.ElasticsearchHealthIndicatorProperties;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.JmsHealthIndicator;
@ -66,6 +61,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadata;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvider;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProviders;
import org.springframework.boot.autoconfigure.jest.JestAutoConfiguration;
import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration;
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
@ -73,7 +69,7 @@ import org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.context.annotation.Import;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
@ -96,12 +92,14 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
@AutoConfigureBefore({ EndpointAutoConfiguration.class })
@AutoConfigureAfter({ CassandraAutoConfiguration.class,
CassandraDataAutoConfiguration.class, CouchbaseAutoConfiguration.class,
DataSourceAutoConfiguration.class, MongoAutoConfiguration.class,
MongoDataAutoConfiguration.class, RedisAutoConfiguration.class,
RabbitAutoConfiguration.class, SolrAutoConfiguration.class,
MailSenderAutoConfiguration.class, JmsAutoConfiguration.class,
ElasticsearchAutoConfiguration.class })
DataSourceAutoConfiguration.class, ElasticsearchAutoConfiguration.class,
JestAutoConfiguration.class, JmsAutoConfiguration.class,
MailSenderAutoConfiguration.class, MongoAutoConfiguration.class,
MongoDataAutoConfiguration.class, RabbitAutoConfiguration.class,
RedisAutoConfiguration.class, SolrAutoConfiguration.class })
@EnableConfigurationProperties({ HealthIndicatorProperties.class })
@Import({ ElasticsearchHealthIndicatorConfiguration.SpringData.class,
ElasticsearchHealthIndicatorConfiguration.Jest.class })
public class HealthIndicatorAutoConfiguration {
private final HealthIndicatorProperties properties;
@ -126,48 +124,6 @@ public class HealthIndicatorAutoConfiguration {
return new ApplicationHealthIndicator();
}
/**
* Base class for configurations that can combine source beans using a
* {@link CompositeHealthIndicator}.
* @param <H> The health indicator type
* @param <S> The bean source type
*/
protected static abstract class CompositeHealthIndicatorConfiguration<H extends HealthIndicator, S> {
@Autowired
private HealthAggregator healthAggregator;
protected HealthIndicator createHealthIndicator(Map<String, S> beans) {
if (beans.size() == 1) {
return createHealthIndicator(beans.values().iterator().next());
}
CompositeHealthIndicator composite = new CompositeHealthIndicator(
this.healthAggregator);
for (Map.Entry<String, S> entry : beans.entrySet()) {
composite.addHealthIndicator(entry.getKey(),
createHealthIndicator(entry.getValue()));
}
return composite;
}
@SuppressWarnings("unchecked")
protected H createHealthIndicator(S source) {
Class<?>[] generics = ResolvableType
.forClass(CompositeHealthIndicatorConfiguration.class, getClass())
.resolveGenerics();
Class<H> indicatorClass = (Class<H>) generics[0];
Class<S> sourceClass = (Class<S>) generics[1];
try {
return indicatorClass.getConstructor(sourceClass).newInstance(source);
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create indicator "
+ indicatorClass + " for source " + sourceClass, ex);
}
}
}
@Configuration
@ConditionalOnClass({ CassandraOperations.class, Cluster.class })
@ConditionalOnBean(CassandraOperations.class)
@ -401,34 +357,4 @@ public class HealthIndicatorAutoConfiguration {
}
@Configuration
@ConditionalOnBean(Client.class)
@ConditionalOnEnabledHealthIndicator("elasticsearch")
@EnableConfigurationProperties(ElasticsearchHealthIndicatorProperties.class)
public static class ElasticsearchHealthIndicatorConfiguration extends
CompositeHealthIndicatorConfiguration<ElasticsearchHealthIndicator, Client> {
private final Map<String, Client> clients;
private final ElasticsearchHealthIndicatorProperties properties;
public ElasticsearchHealthIndicatorConfiguration(Map<String, Client> clients,
ElasticsearchHealthIndicatorProperties properties) {
this.clients = clients;
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(name = "elasticsearchHealthIndicator")
public HealthIndicator elasticsearchHealthIndicator() {
return createHealthIndicator(this.clients);
}
@Override
protected ElasticsearchHealthIndicator createHealthIndicator(Client client) {
return new ElasticsearchHealthIndicator(client, this.properties);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.health;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.indices.Stats;
/**
* {@link HealthIndicator} for Elasticsearch using a {@link JestClient}.
*
* @author Stephane Nicoll
* @since 1.4.0
*/
public class ElasticsearchJestHealthIndicator extends AbstractHealthIndicator {
private final JestClient jestClient;
public ElasticsearchJestHealthIndicator(JestClient jestClient) {
this.jestClient = jestClient;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
JestResult aliases = this.jestClient.execute(new Stats.Builder().build());
JsonParser jsonParser = new JsonParser();
JsonElement root = jsonParser.parse(aliases.getJsonString());
JsonObject shards = root.getAsJsonObject().get("_shards").getAsJsonObject();
int failedShards = shards.get("failed").getAsInt();
if (failedShards != 0) {
builder.outOfService();
}
else {
builder.up();
}
}
}

View File

@ -20,6 +20,7 @@ import java.util.Map;
import javax.sql.DataSource;
import io.searchbox.client.JestClient;
import org.junit.After;
import org.junit.Test;
@ -29,6 +30,7 @@ import org.springframework.boot.actuate.health.CouchbaseHealthIndicator;
import org.springframework.boot.actuate.health.DataSourceHealthIndicator;
import org.springframework.boot.actuate.health.DiskSpaceHealthIndicator;
import org.springframework.boot.actuate.health.ElasticsearchHealthIndicator;
import org.springframework.boot.actuate.health.ElasticsearchJestHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.JmsHealthIndicator;
@ -46,6 +48,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration;
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration;
import org.springframework.boot.autoconfigure.jest.JestAutoConfiguration;
import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration;
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
@ -395,7 +398,8 @@ public class HealthIndicatorAutoConfigurationTests {
EnvironmentTestUtils.addEnvironment(this.context,
"spring.data.elasticsearch.properties.path.home:target",
"management.health.diskspace.enabled:false");
this.context.register(ElasticsearchAutoConfiguration.class,
this.context.register(JestClientConfiguration.class,
JestAutoConfiguration.class, ElasticsearchAutoConfiguration.class,
ManagementServerProperties.class, HealthIndicatorAutoConfiguration.class);
this.context.refresh();
@ -406,13 +410,30 @@ public class HealthIndicatorAutoConfigurationTests {
.isEqualTo(ElasticsearchHealthIndicator.class);
}
@Test
public void elasticSearchJestHealthIndicator() {
EnvironmentTestUtils.addEnvironment(this.context,
"management.health.diskspace.enabled:false");
this.context.register(JestClientConfiguration.class,
JestAutoConfiguration.class, ManagementServerProperties.class,
HealthIndicatorAutoConfiguration.class);
this.context.refresh();
Map<String, HealthIndicator> beans = this.context
.getBeansOfType(HealthIndicator.class);
assertThat(beans).hasSize(1);
assertThat(beans.values().iterator().next().getClass())
.isEqualTo(ElasticsearchJestHealthIndicator.class);
}
@Test
public void notElasticSearchHealthIndicator() {
EnvironmentTestUtils.addEnvironment(this.context,
"management.health.elasticsearch.enabled:false",
"spring.data.elasticsearch.properties.path.home:target",
"management.health.diskspace.enabled:false");
this.context.register(ElasticsearchAutoConfiguration.class,
this.context.register(JestClientConfiguration.class,
JestAutoConfiguration.class, ElasticsearchAutoConfiguration.class,
ManagementServerProperties.class, HealthIndicatorAutoConfiguration.class);
this.context.refresh();
@ -502,4 +523,12 @@ public class HealthIndicatorAutoConfigurationTests {
}
protected static class JestClientConfiguration {
@Bean
public JestClient jestClient() {
return mock(JestClient.class);
}
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.health;
import java.io.IOException;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import io.searchbox.action.Action;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.client.config.exception.CouldNotConnectException;
import io.searchbox.core.SearchResult;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ElasticsearchJestHealthIndicator}.
*
* @author Stephane Nicoll
*/
public class ElasticsearchJestHealthIndicatorTests {
private final JestClient jestClient = mock(JestClient.class);
private final ElasticsearchJestHealthIndicator healthIndicator =
new ElasticsearchJestHealthIndicator(this.jestClient);
@Test
public void elasticSearchIsUp() throws IOException {
given(this.jestClient.execute(any(Action.class)))
.willReturn(createJestResult(4, 0));
Health health = this.healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
}
@Test
public void elasticSearchIsDown() throws IOException {
given(this.jestClient.execute(any(Action.class))).willThrow(
new CouldNotConnectException("http://localhost:9200", new IOException()));
Health health = this.healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
}
@Test
public void elasticSearchIsOutOfService() throws IOException {
given(this.jestClient.execute(any(Action.class)))
.willReturn(createJestResult(4, 1));
Health health = this.healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
}
private static JestResult createJestResult(int shards, int failedShards) {
String json = String.format("{_shards: {\n" +
"total: %s,\n" +
"successful: %s,\n" +
"failed: %s\n" +
"}}", shards, shards - failedShards, failedShards);
SearchResult searchResult = new SearchResult(new Gson());
searchResult.setJsonString(json);
searchResult.setJsonObject(new JsonParser().parse(json).getAsJsonObject());
return searchResult;
}
}