diff --git a/spring-boot-actuator/.gitignore b/spring-boot-actuator/.gitignore new file mode 100644 index 0000000000..3af0ccb687 --- /dev/null +++ b/spring-boot-actuator/.gitignore @@ -0,0 +1 @@ +/data diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 508e125c14..924506687e 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -96,6 +96,11 @@ spring-data-solr true + + org.elasticsearch + elasticsearch + true + org.springframework.security spring-security-web @@ -168,19 +173,25 @@ true + + org.springframework.boot + spring-boot + test-jar + test + ch.qos.logback logback-classic test - org.crashub - crash.connectors.telnet + org.apache.tomcat.embed + tomcat-embed-logging-juli test - org.springframework - spring-test + org.crashub + crash.connectors.telnet test @@ -189,14 +200,13 @@ test - org.springframework.boot - spring-boot - test-jar + org.springframework + spring-test test - org.apache.tomcat.embed - tomcat-embed-logging-juli + org.springframework.data + spring-data-elasticsearch test diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java index 7a1790e17f..518444f3bb 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java @@ -23,6 +23,7 @@ import javax.jms.ConnectionFactory; import javax.sql.DataSource; import org.apache.solr.client.solrj.SolrServer; +import org.elasticsearch.client.Client; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +32,8 @@ import org.springframework.boot.actuate.health.CompositeHealthIndicator; 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; @@ -47,6 +50,7 @@ import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadata; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvider; @@ -79,7 +83,8 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, RedisAutoConfiguration.class, RabbitAutoConfiguration.class, SolrAutoConfiguration.class, - MailSenderAutoConfiguration.class, JmsAutoConfiguration.class }) + MailSenderAutoConfiguration.class, JmsAutoConfiguration.class, + ElasticsearchAutoConfiguration.class }) @EnableConfigurationProperties({ HealthIndicatorAutoConfigurationProperties.class }) public class HealthIndicatorAutoConfiguration { @@ -306,4 +311,30 @@ public class HealthIndicatorAutoConfiguration { } + @Configuration + @ConditionalOnBean(Client.class) + @ConditionalOnProperty(prefix = "management.health.elasticsearch", name = "enabled", matchIfMissing = true) + @EnableConfigurationProperties(ElasticsearchHealthIndicatorProperties.class) + public static class ElasticsearchHealthIndicatorConfiguration extends + CompositeHealthIndicatorConfiguration { + + @Autowired + private Map clients; + + @Autowired + private ElasticsearchHealthIndicatorProperties properties; + + @Bean + @ConditionalOnMissingBean(name = "elasticsearchHealthIndicator") + public HealthIndicator elasticsearchHealthIndicator() { + return createHealthIndicator(this.clients); + } + + @Override + protected ElasticsearchHealthIndicator createHealthIndicator(Client client) { + return new ElasticsearchHealthIndicator(client, this.properties); + } + + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicator.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicator.java new file mode 100644 index 0000000000..55b424d64d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicator.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2015 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.util.List; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.Requests; + +/** + * {@link HealthIndicator} for an Elasticsearch cluster. + * + * @author Binwei Yang + * @author Andy Wilkinson + * @since 1.3.0 + */ +public class ElasticsearchHealthIndicator extends AbstractHealthIndicator { + + private final Client client; + + private final ElasticsearchHealthIndicatorProperties properties; + + public ElasticsearchHealthIndicator(Client client, + ElasticsearchHealthIndicatorProperties properties) { + this.client = client; + this.properties = properties; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + List indices = this.properties.getIndices(); + ClusterHealthResponse response = this.client + .admin() + .cluster() + .health(Requests.clusterHealthRequest(indices.isEmpty() ? null : indices + .toArray(new String[indices.size()]))) + .actionGet(this.properties.getResponseTimeout()); + + switch (response.getStatus()) { + case GREEN: + case YELLOW: + builder.up(); + break; + case RED: + default: + builder.down(); + break; + } + builder.withDetail("clusterName", response.getClusterName()); + builder.withDetail("numberOfNodes", response.getNumberOfNodes()); + builder.withDetail("numberOfDataNodes", response.getNumberOfDataNodes()); + builder.withDetail("activePrimaryShards", response.getActivePrimaryShards()); + builder.withDetail("activeShards", response.getActiveShards()); + builder.withDetail("relocatingShards", response.getRelocatingShards()); + builder.withDetail("initializingShards", response.getInitializingShards()); + builder.withDetail("unassignedShards", response.getUnassignedShards()); + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicatorProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicatorProperties.java new file mode 100644 index 0000000000..2c52c8c865 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicatorProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2015 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.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * External configuration properties for {@link ElasticsearchHealthIndicator} + * + * @author Binwei Yang + * @author Andy Wilkinson + * @since 1.3.0 + */ +@ConfigurationProperties("management.health.elasticsearch") +public class ElasticsearchHealthIndicatorProperties { + + /** + * Comma-separated index names + */ + private List indices = new ArrayList(); + + /** + * The time, in milliseconds, to wait for a response from the cluster + */ + private long responseTimeout = 100L; + + public List getIndices() { + return this.indices; + } + + public long getResponseTimeout() { + return this.responseTimeout; + } + + public void setResponseTimeout(long responseTimeout) { + this.responseTimeout = responseTimeout; + } + +} diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index 799422da3c..6d4f6a7867 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -12,4 +12,4 @@ org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.PublicMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.TraceRepositoryAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration +org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration \ No newline at end of file diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java index 251214a9aa..91171bbf06 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfigurationTests.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; 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.HealthIndicator; import org.springframework.boot.actuate.health.JmsHealthIndicator; import org.springframework.boot.actuate.health.MailHealthIndicator; @@ -35,6 +36,7 @@ import org.springframework.boot.actuate.health.RedisHealthIndicator; import org.springframework.boot.actuate.health.SolrHealthIndicator; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; @@ -59,6 +61,7 @@ import static org.junit.Assert.assertEquals; * * @author Christian Dupuis * @author Stephane Nicoll + * @author Andy Wilkinson */ public class HealthIndicatorAutoConfigurationTests { @@ -362,6 +365,39 @@ public class HealthIndicatorAutoConfigurationTests { .getClass()); } + @Test + public void elasticSearchHealthIndicator() { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "management.health.diskspace.enabled:false"); + this.context.register(ElasticsearchAutoConfiguration.class, + ManagementServerProperties.class, HealthIndicatorAutoConfiguration.class); + this.context.refresh(); + + Map beans = this.context + .getBeansOfType(HealthIndicator.class); + assertEquals(1, beans.size()); + assertEquals(ElasticsearchHealthIndicator.class, beans.values().iterator().next() + .getClass()); + } + + @Test + public void notElasticSearchHealthIndicator() { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "management.health.elasticsearch.enabled:false", + "management.health.diskspace.enabled:false"); + this.context.register(ElasticsearchAutoConfiguration.class, + ManagementServerProperties.class, HealthIndicatorAutoConfiguration.class); + this.context.refresh(); + + Map beans = this.context + .getBeansOfType(HealthIndicator.class); + assertEquals(1, beans.size()); + assertEquals(ApplicationHealthIndicator.class, beans.values().iterator().next() + .getClass()); + } + @Configuration @EnableConfigurationProperties protected static class DataSourceConfig { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicatorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicatorTests.java new file mode 100644 index 0000000000..611ca1086f --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ElasticsearchHealthIndicatorTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2012-2015 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.util.Arrays; +import java.util.Map; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchTimeoutException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.AdminClient; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.ClusterAdminClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; + +/** + * Test for {@link ElasticsearchHealthIndicator}. + * + * @author Andy Wilkinson + */ +@RunWith(MockitoJUnitRunner.class) +public class ElasticsearchHealthIndicatorTests { + + @Mock + private Client client; + + @Mock + private AdminClient admin; + + @Mock + private ClusterAdminClient cluster; + + private ElasticsearchHealthIndicator indicator; + + private ElasticsearchHealthIndicatorProperties properties = new ElasticsearchHealthIndicatorProperties(); + + @Before + public void setUp() throws Exception { + given(this.client.admin()).willReturn(this.admin); + given(this.admin.cluster()).willReturn(this.cluster); + + this.indicator = new ElasticsearchHealthIndicator(this.client, this.properties); + } + + @Test + public void defaultConfigurationQueriesAllIndicesWith100msTimeout() { + TestActionFuture responseFuture = new TestActionFuture(); + responseFuture.onResponse(new StubClusterHealthResponse()); + ArgumentCaptor requestCaptor = ArgumentCaptor + .forClass(ClusterHealthRequest.class); + given(this.cluster.health(requestCaptor.capture())).willReturn(responseFuture); + Health health = this.indicator.health(); + assertThat(responseFuture.getTimeout, is(100L)); + assertThat(requestCaptor.getValue().indices(), is(nullValue())); + assertThat(health.getStatus(), is(Status.UP)); + } + + @Test + public void certainIndices() { + PlainActionFuture responseFuture = new PlainActionFuture(); + responseFuture.onResponse(new StubClusterHealthResponse()); + ArgumentCaptor requestCaptor = ArgumentCaptor + .forClass(ClusterHealthRequest.class); + given(this.cluster.health(requestCaptor.capture())).willReturn(responseFuture); + this.properties.getIndices() + .addAll(Arrays.asList("test-index-1", "test-index-2")); + Health health = this.indicator.health(); + assertThat(requestCaptor.getValue().indices(), + is(arrayContaining("test-index-1", "test-index-2"))); + assertThat(health.getStatus(), is(Status.UP)); + } + + @Test + public void customTimeout() { + TestActionFuture responseFuture = new TestActionFuture(); + responseFuture.onResponse(new StubClusterHealthResponse()); + ArgumentCaptor requestCaptor = ArgumentCaptor + .forClass(ClusterHealthRequest.class); + given(this.cluster.health(requestCaptor.capture())).willReturn(responseFuture); + this.properties.setResponseTimeout(1000L); + this.indicator.health(); + assertThat(responseFuture.getTimeout, is(1000L)); + } + + @Test + public void healthDetails() { + PlainActionFuture responseFuture = new PlainActionFuture(); + responseFuture.onResponse(new StubClusterHealthResponse()); + given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn( + responseFuture); + Health health = this.indicator.health(); + assertThat(health.getStatus(), is(Status.UP)); + Map details = health.getDetails(); + assertDetail(details, "clusterName", "test-cluster"); + assertDetail(details, "activeShards", 1); + assertDetail(details, "relocatingShards", 2); + assertDetail(details, "activePrimaryShards", 3); + assertDetail(details, "initializingShards", 4); + assertDetail(details, "unassignedShards", 5); + assertDetail(details, "numberOfNodes", 6); + assertDetail(details, "numberOfDataNodes", 7); + } + + @Test + public void redResponseMapsToDown() { + PlainActionFuture responseFuture = new PlainActionFuture(); + responseFuture.onResponse(new StubClusterHealthResponse(ClusterHealthStatus.RED)); + given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn( + responseFuture); + assertThat(this.indicator.health().getStatus(), is(Status.DOWN)); + } + + @Test + public void yellowResponseMapsToUp() { + PlainActionFuture responseFuture = new PlainActionFuture(); + responseFuture.onResponse(new StubClusterHealthResponse( + ClusterHealthStatus.YELLOW)); + given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn( + responseFuture); + assertThat(this.indicator.health().getStatus(), is(Status.UP)); + } + + @Test + public void responseTimeout() { + PlainActionFuture responseFuture = new PlainActionFuture(); + given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn( + responseFuture); + Health health = this.indicator.health(); + assertThat(health.getStatus(), is(Status.DOWN)); + assertThat((String) health.getDetails().get("error"), + containsString(ElasticsearchTimeoutException.class.getName())); + } + + @SuppressWarnings("unchecked") + private void assertDetail(Map details, String detail, T value) { + assertThat((T) details.get(detail), is(equalTo(value))); + } + + private static class StubClusterHealthResponse extends ClusterHealthResponse { + + private final ClusterHealthStatus status; + + private StubClusterHealthResponse() { + this(ClusterHealthStatus.GREEN); + } + + private StubClusterHealthResponse(ClusterHealthStatus status) { + super("test-cluster", null); + this.status = status; + } + + @Override + public int getActiveShards() { + return 1; + } + + @Override + public int getRelocatingShards() { + return 2; + } + + @Override + public int getActivePrimaryShards() { + return 3; + } + + @Override + public int getInitializingShards() { + return 4; + } + + @Override + public int getUnassignedShards() { + return 5; + } + + @Override + public int getNumberOfNodes() { + return 6; + } + + @Override + public int getNumberOfDataNodes() { + return 7; + } + + @Override + public ClusterHealthStatus getStatus() { + return this.status; + } + + } + + private static class TestActionFuture extends + PlainActionFuture { + + private long getTimeout = -1L; + + @Override + public ClusterHealthResponse actionGet(long timeoutMillis) + throws ElasticsearchException { + this.getTimeout = timeoutMillis; + return super.actionGet(timeoutMillis); + } + + } +} diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 7fb6ae32c1..a32ec83c1b 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -63,6 +63,7 @@ 2.9.1 3.2 2.3.22 + 1.4.4 7.0.2 3.0.0 1.12 @@ -1084,6 +1085,11 @@ websocket-server ${jetty.version} + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + org.flywaydb flyway-core diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 5714f57996..31bb2ab0f3 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -542,13 +542,15 @@ content into your application; rather pick only the properties that you need. # HEALTH INDICATORS (previously health.*) management.health.db.enabled=true + management.health.elasticsearch.enabled=true + management.health.elasticsearch.response-timeout=100 # the time, in milliseconds, to wait for a response from the cluster management.health.diskspace.enabled=true + management.health.diskspace.path=. + management.health.diskspace.threshold=10485760 management.health.mongo.enabled=true management.health.rabbit.enabled=true management.health.redis.enabled=true management.health.solr.enabled=true - management.health.diskspace.path=. - management.health.diskspace.threshold=10485760 management.health.status.order=DOWN, OUT_OF_SERVICE, UNKNOWN, UP # MVC ONLY ENDPOINTS