From 4c97dcb53ae0a7cba4859ba60af2bf6474bad39f Mon Sep 17 00:00:00 2001 From: Eric Spiegelberg Date: Mon, 19 Jun 2017 19:54:16 -0500 Subject: [PATCH] Add health indicator for Neo4j See gh-9557 --- .../HealthIndicatorAutoConfiguration.java | 28 ++++- .../actuate/health/Neo4jHealthIndicator.java | 64 +++++++++++ ...itional-spring-configuration-metadata.json | 6 ++ ...HealthIndicatorAutoConfigurationTests.java | 41 +++++++ .../health/Neo4jHealthIndicatorTests.java | 100 ++++++++++++++++++ 5 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Neo4jHealthIndicator.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/Neo4jHealthIndicatorTests.java 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 95d70080195..6b1c5c528a1 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 @@ -26,6 +26,7 @@ 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.neo4j.ogm.session.SessionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.InitializingBean; @@ -42,6 +43,7 @@ import org.springframework.boot.actuate.health.JmsHealthIndicator; import org.springframework.boot.actuate.health.LdapHealthIndicator; import org.springframework.boot.actuate.health.MailHealthIndicator; import org.springframework.boot.actuate.health.MongoHealthIndicator; +import org.springframework.boot.actuate.health.Neo4jHealthIndicator; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.RabbitHealthIndicator; import org.springframework.boot.actuate.health.RedisHealthIndicator; @@ -59,6 +61,7 @@ import org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoCo import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration; import org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @@ -91,6 +94,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; * @author Phillip Webb * @author Tommy Ludwig * @author Eddú Meléndez + * @author Eric Spiegelberg * @since 1.1.0 */ @Configuration @@ -102,7 +106,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; LdapDataAutoConfiguration.class, MailSenderAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, RabbitAutoConfiguration.class, RedisAutoConfiguration.class, - SolrAutoConfiguration.class }) + SolrAutoConfiguration.class, Neo4jDataAutoConfiguration.class }) @EnableConfigurationProperties({ HealthIndicatorProperties.class }) @Import({ ElasticsearchHealthIndicatorConfiguration.ElasticsearchClientHealthIndicatorConfiguration.class, @@ -257,6 +261,28 @@ public class HealthIndicatorAutoConfiguration { } + @Configuration + @ConditionalOnClass(SessionFactory.class) + @ConditionalOnBean(SessionFactory.class) + @ConditionalOnEnabledHealthIndicator("neo4j") + public static class Neo4jHealthIndicatorConfiguration extends + CompositeHealthIndicatorConfiguration { + + private final Map sessionFactories; + + public Neo4jHealthIndicatorConfiguration( + Map sessionFactories) { + this.sessionFactories = sessionFactories; + } + + @Bean + @ConditionalOnMissingBean(name = "neo4jHealthIndicator") + public HealthIndicator neo4jHealthIndicator() { + return createHealthIndicator(this.sessionFactories); + } + + } + @Configuration @ConditionalOnBean(MongoTemplate.class) @ConditionalOnEnabledHealthIndicator("mongo") diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Neo4jHealthIndicator.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Neo4jHealthIndicator.java new file mode 100644 index 00000000000..3292bc95777 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Neo4jHealthIndicator.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2017 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.Collections; +import java.util.Map; + +import org.neo4j.ogm.model.Result; +import org.neo4j.ogm.session.Session; +import org.neo4j.ogm.session.SessionFactory; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link HealthIndicator} that tests the status of a Neo4j by executing a Cypher + * statement. + * + * @author Eric Spiegelberg + */ +@ConfigurationProperties(prefix = "management.health.neo4j", ignoreUnknownFields = false) +public class Neo4jHealthIndicator extends AbstractHealthIndicator { + + private final SessionFactory sessionFactory; + + /** + * The Cypher statement used to verify Neo4j is up. + */ + public static final String CYPHER = "match (n) return count(n) as nodes"; + + /** + * Create a new {@link Neo4jHealthIndicator} using the specified + * {@link SessionFactory}. + * @param sessionFactory the SessionFactory + */ + public Neo4jHealthIndicator(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + Session session = this.sessionFactory.openSession(); + + Result result = session.query(CYPHER, Collections.emptyMap()); + Iterable> results = result.queryResults(); + int nodes = (int) results.iterator().next().get("nodes"); + + builder.up().withDetail("nodes", nodes); + } + +} diff --git a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 4ad754d3695..4e7549cbb20 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -181,6 +181,12 @@ "description": "Enable Mail health check.", "defaultValue": true }, + { + "name": "management.health.neo4j.enabled", + "type": "java.lang.Boolean", + "description": "Enable Neo4j health check.", + "defaultValue": true + }, { "name": "management.info.build.enabled", "type": "java.lang.Boolean", 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 98bc14741b6..1da53430f0a 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 @@ -23,6 +23,7 @@ import javax.sql.DataSource; import io.searchbox.client.JestClient; import org.junit.After; import org.junit.Test; +import org.neo4j.ogm.session.SessionFactory; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; import org.springframework.boot.actuate.health.CassandraHealthIndicator; @@ -38,6 +39,7 @@ import org.springframework.boot.actuate.health.JmsHealthIndicator; import org.springframework.boot.actuate.health.LdapHealthIndicator; import org.springframework.boot.actuate.health.MailHealthIndicator; import org.springframework.boot.actuate.health.MongoHealthIndicator; +import org.springframework.boot.actuate.health.Neo4jHealthIndicator; import org.springframework.boot.actuate.health.RabbitHealthIndicator; import org.springframework.boot.actuate.health.RedisHealthIndicator; import org.springframework.boot.actuate.health.SolrHealthIndicator; @@ -76,6 +78,7 @@ import static org.mockito.Mockito.mock; * @author Stephane Nicoll * @author Andy Wilkinson * @author Eddú Meléndez + * @author Eric Spiegelberg */ public class HealthIndicatorAutoConfigurationTests { @@ -578,6 +581,34 @@ public class HealthIndicatorAutoConfigurationTests { .isEqualTo(ApplicationHealthIndicator.class); } + @Test + public void neo4jHealthIndicator() throws Exception { + TestPropertyValues.of("management.health.diskspace.enabled:false") + .applyTo(this.context); + this.context.register(Neo4jConfiguration.class, ManagementServerProperties.class, + HealthIndicatorAutoConfiguration.class); + this.context.refresh(); + Map beans = this.context + .getBeansOfType(HealthIndicator.class); + assertThat(beans.size()).isEqualTo(1); + assertThat(beans.values().iterator().next().getClass()) + .isEqualTo(Neo4jHealthIndicator.class); + } + + @Test + public void notNeo4jHealthIndicator() throws Exception { + TestPropertyValues.of("management.health.diskspace.enabled:false", + "management.health.neo4j.enabled:false").applyTo(this.context); + this.context.register(Neo4jConfiguration.class, ManagementServerProperties.class, + HealthIndicatorAutoConfiguration.class); + this.context.refresh(); + Map beans = this.context + .getBeansOfType(HealthIndicator.class); + assertThat(beans.size()).isEqualTo(1); + assertThat(beans.values().iterator().next().getClass()) + .isEqualTo(ApplicationHealthIndicator.class); + } + @Configuration @EnableConfigurationProperties protected static class DataSourceConfig { @@ -659,4 +690,14 @@ public class HealthIndicatorAutoConfigurationTests { } + @Configuration + protected static class Neo4jConfiguration { + + @Bean + public SessionFactory sessionFactory() { + return mock(SessionFactory.class); + } + + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/Neo4jHealthIndicatorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/Neo4jHealthIndicatorTests.java new file mode 100644 index 00000000000..fa4983e417a --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/Neo4jHealthIndicatorTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2017 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.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.neo4j.ogm.exception.CypherException; +import org.neo4j.ogm.model.Result; +import org.neo4j.ogm.session.Session; +import org.neo4j.ogm.session.SessionFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Neo4jHealthIndicator}. + * + * @author Eric Spiegelberg + */ +public class Neo4jHealthIndicatorTests { + + private Result result; + private Session session; + private SessionFactory sessionFactory; + + private Neo4jHealthIndicator neo4jHealthIndicator; + + private Map emptyParameters = new HashMap<>(); + + @Before + public void before() { + this.result = mock(Result.class); + this.session = mock(Session.class); + this.sessionFactory = mock(SessionFactory.class); + + given(this.sessionFactory.openSession()).willReturn(this.session); + + this.neo4jHealthIndicator = new Neo4jHealthIndicator(this.sessionFactory); + } + + @Test + public void neo4jUp() { + given(this.session.query(Neo4jHealthIndicator.CYPHER, this.emptyParameters)) + .willReturn(this.result); + + int nodeCount = 500; + Map expectedCypherDetails = new HashMap<>(); + expectedCypherDetails.put("nodes", nodeCount); + + List> queryResults = new ArrayList<>(); + queryResults.add(expectedCypherDetails); + + given(this.result.queryResults()).willReturn(queryResults); + + Health health = this.neo4jHealthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + + Map details = health.getDetails(); + int nodeCountFromDetails = (int) details.get("nodes"); + + Assert.assertEquals(nodeCount, nodeCountFromDetails); + + } + + @Test + public void neo4jDown() { + + CypherException cypherException = new CypherException("Error executing Cypher", + "Neo.ClientError.Statement.SyntaxError", + "Unable to execute invalid Cypher"); + + given(this.session.query(Neo4jHealthIndicator.CYPHER, this.emptyParameters)) + .willThrow(cypherException); + + Health health = this.neo4jHealthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + +}