diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index bf42411621..3898b33774 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -86,6 +86,7 @@ dependencies { optional("org.liquibase:liquibase-core") optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-sync") + optional("org.neo4j.driver:neo4j-java-driver") optional("org.springframework:spring-jdbc") optional("org.springframework:spring-jms") optional("org.springframework:spring-messaging") @@ -96,7 +97,6 @@ dependencies { optional("org.springframework.data:spring-data-couchbase") optional("org.springframework.data:spring-data-ldap") optional("org.springframework.data:spring-data-mongodb") - optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-solr") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java index 9f24c18323..7730ddf629 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -16,42 +16,36 @@ package org.springframework.boot.actuate.autoconfigure.neo4j; -import java.util.Map; +import org.neo4j.driver.Driver; -import org.neo4j.ogm.session.SessionFactory; - -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorConfigurations.Neo4jConfiguration; +import org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorConfigurations.Neo4jReactiveConfiguration; import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** - * {@link EnableAutoConfiguration Auto-configuration} for {@link Neo4jHealthIndicator}. + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link Neo4jReactiveHealthIndicator} and {@link Neo4jHealthIndicator}. * * @author Eric Spiegelberg * @author Stephane Nicoll + * @author Michael J. Simons * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(SessionFactory.class) -@ConditionalOnBean(SessionFactory.class) +@ConditionalOnClass(Driver.class) +@ConditionalOnBean(Driver.class) @ConditionalOnEnabledHealthIndicator("neo4j") -@AutoConfigureAfter(Neo4jDataAutoConfiguration.class) -public class Neo4jHealthContributorAutoConfiguration - extends CompositeHealthContributorConfiguration { - - @Bean - @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) - public HealthContributor neo4jHealthContributor(Map sessionFactories) { - return createContributor(sessionFactories); - } +@AutoConfigureAfter(Neo4jAutoConfiguration.class) +@Import({ Neo4jReactiveConfiguration.class, Neo4jConfiguration.class }) +public class Neo4jHealthContributorAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java new file mode 100644 index 0000000000..e53e14d27c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java @@ -0,0 +1,67 @@ +/* + * 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.neo4j; + +import java.util.Map; + +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; +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; + +/** + * Health contributor options for Neo4j. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jHealthContributorConfigurations { + + @Configuration(proxyBeanMethods = false) + static class Neo4jConfiguration extends CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) + HealthContributor neo4jHealthContributor(Map drivers) { + return createContributor(drivers); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Flux.class) + static class Neo4jReactiveConfiguration + extends CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) + ReactiveHealthContributor neo4jHealthContributor(Map drivers) { + return createContributor(drivers); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java index 6e11d71c1f..7191701934 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java index c7fdf0c932..5de0389b31 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoCo import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.util.ApplicationContextTestUtils; import org.springframework.context.ConfigurableApplicationContext; @@ -69,7 +70,7 @@ class SpringApplicationHierarchyTests { @EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class, ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class, - MongoReactiveDataAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }) static class Parent { @@ -80,7 +81,7 @@ class SpringApplicationHierarchyTests { @EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class, ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class, - MongoReactiveDataAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }) static class Child { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java index b768bc2faf..8e18f2e380 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.boot.actuate.autoconfigure.neo4j; import org.junit.jupiter.api.Test; -import org.neo4j.ogm.session.Session; -import org.neo4j.ogm.session.SessionFactory; +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +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.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; +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; @@ -37,39 +40,57 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Stephane Nicoll + * @author Michael J. Simons */ class Neo4jHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(Neo4jConfiguration.class).withConfiguration(AutoConfigurations - .of(Neo4jHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class, + Neo4jHealthContributorAutoConfiguration.class)); @Test - void runShouldCreateIndicator() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Neo4jHealthIndicator.class)); + void runShouldCreateHealthIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(Neo4jReactiveHealthIndicator.class).doesNotHaveBean(Neo4jHealthIndicator.class)); + } + + @Test + void runWithoutReactorShouldCreateHealthIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class) + .withClassLoader(new FilteredClassLoader(Flux.class)).run((context) -> assertThat(context) + .hasSingleBean(Neo4jHealthIndicator.class).doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Test void runWhenDisabledShouldNotCreateIndicator() { - this.contextRunner.withPropertyValues("management.health.neo4j.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class)); + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class) + .withPropertyValues("management.health.neo4j.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class) + .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Test void defaultIndicatorCanBeReplaced() { - this.contextRunner.withUserConfiguration(CustomIndicatorConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(Neo4jHealthIndicator.class); - Health health = context.getBean(Neo4jHealthIndicator.class).health(); - assertThat(health.getDetails()).containsOnly(entry("test", true)); - }); + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class, CustomIndicatorConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("neo4jHealthIndicator"); + Health health = context.getBean("neo4jHealthIndicator", HealthIndicator.class).health(); + assertThat(health.getDetails()).containsOnly(entry("test", true)); + }); + } + + @Test + void shouldRequireDriverBean() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class) + .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Configuration(proxyBeanMethods = false) static class Neo4jConfiguration { @Bean - SessionFactory sessionFactory() { - return mock(SessionFactory.class); + Driver driver() { + return mock(Driver.class); } } @@ -78,14 +99,12 @@ class Neo4jHealthContributorAutoConfigurationTests { static class CustomIndicatorConfiguration { @Bean - Neo4jHealthIndicator neo4jHealthIndicator(SessionFactory sessionFactory) { - return new Neo4jHealthIndicator(sessionFactory) { + HealthIndicator neo4jHealthIndicator() { + return new AbstractHealthIndicator() { - @Override - protected void extractResult(Session session, Health.Builder builder) { + protected void doHealthCheck(Health.Builder builder) { builder.up().withDetail("test", true); } - }; } diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 9f188d3827..8545696e91 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -46,6 +46,7 @@ dependencies { optional("org.liquibase:liquibase-core") optional("org.mongodb:mongodb-driver-reactivestreams") optional("org.mongodb:mongodb-driver-sync") + optional("org.neo4j.driver:neo4j-java-driver") optional("org.springframework:spring-jdbc") optional("org.springframework:spring-messaging") optional("org.springframework:spring-webflux") @@ -57,7 +58,6 @@ dependencies { optional("org.springframework.data:spring-data-elasticsearch") optional("org.springframework.data:spring-data-ldap") optional("org.springframework.data:spring-data-mongodb") - optional("org.springframework.data:spring-data-neo4j") optional("org.springframework.data:spring-data-redis") optional("org.springframework.data:spring-data-rest-webmvc") optional("org.springframework.data:spring-data-solr") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java new file mode 100644 index 0000000000..5bf908a3cb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java @@ -0,0 +1,49 @@ +/* + * 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.neo4j; + +import org.neo4j.driver.summary.DatabaseInfo; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.util.StringUtils; + +/** + * Handle health check details for a Neo4j server. + * + * @author Stephane Nicoll + */ +class Neo4jHealthDetailsHandler { + + /** + * Add health details for the specified {@link ResultSummary} and {@code edition}. + * @param builder the {@link Builder} to use + * @param edition the edition of the server + * @param resultSummary server information + */ + void addHealthDetails(Builder builder, String edition, ResultSummary resultSummary) { + ServerInfo serverInfo = resultSummary.server(); + builder.up().withDetail("server", serverInfo.version() + "@" + serverInfo.address()).withDetail("edition", + edition); + DatabaseInfo databaseInfo = resultSummary.database(); + if (StringUtils.hasText(databaseInfo.name())) { + builder.withDetail("database", databaseInfo.name()); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java index 3f972070b5..782be4cb2b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java @@ -16,61 +16,85 @@ package org.springframework.boot.actuate.neo4j; -import java.util.Collections; - -import org.neo4j.ogm.model.Result; -import org.neo4j.ogm.session.Session; -import org.neo4j.ogm.session.SessionFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.summary.ResultSummary; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.HealthIndicator; /** * {@link HealthIndicator} that tests the status of a Neo4j by executing a Cypher - * statement. + * statement and extracting server and database information. * * @author Eric Spiegelberg * @author Stephane Nicoll + * @author Michael J. Simons * @since 2.0.0 */ public class Neo4jHealthIndicator extends AbstractHealthIndicator { + private static final Log logger = LogFactory.getLog(Neo4jHealthIndicator.class); + /** * The Cypher statement used to verify Neo4j is up. */ - static final String CYPHER = "CALL dbms.components() YIELD versions, edition" - + " UNWIND versions as version return version, edition"; - - private final SessionFactory sessionFactory; + static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition"; /** - * Create a new {@link Neo4jHealthIndicator} using the specified - * {@link SessionFactory}. - * @param sessionFactory the SessionFactory + * Message logged before retrying a health check. */ - public Neo4jHealthIndicator(SessionFactory sessionFactory) { - super("Neo4J health check failed"); - this.sessionFactory = sessionFactory; + static final String MESSAGE_SESSION_EXPIRED = "Neo4j session has expired, retrying one single time to retrieve server health."; + + /** + * The default session config to use while connecting. + */ + static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder().withDefaultAccessMode(AccessMode.WRITE) + .build(); + + private final Driver driver; + + private final Neo4jHealthDetailsHandler healthDetailsHandler; + + public Neo4jHealthIndicator(Driver driver) { + super("Neo4j health check failed"); + this.driver = driver; + this.healthDetailsHandler = new Neo4jHealthDetailsHandler(); } @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - Session session = this.sessionFactory.openSession(); - extractResult(session, builder); + protected void doHealthCheck(Health.Builder builder) { + try { + try { + runHealthCheckQuery(builder); + } + catch (SessionExpiredException ex) { + // Retry one time when the session has been expired + logger.warn(MESSAGE_SESSION_EXPIRED); + runHealthCheckQuery(builder); + } + } + catch (Exception ex) { + builder.down().withException(ex); + } } - /** - * Provide health details using the specified {@link Session} and {@link Builder - * Builder}. - * @param session the session to use to execute a cypher statement - * @param builder the builder to add details to - * @throws Exception if getting health details failed - */ - protected void extractResult(Session session, Health.Builder builder) throws Exception { - Result result = session.query(CYPHER, Collections.emptyMap()); - builder.up().withDetails(result.queryResults().iterator().next()); + private void runHealthCheckQuery(Health.Builder builder) { + // We use WRITE here to make sure UP is returned for a server that supports + // all possible workloads + try (Session session = this.driver.session(DEFAULT_SESSION_CONFIG)) { + Result result = session.run(CYPHER); + String edition = result.single().get("edition").asString(); + ResultSummary resultSummary = result.consume(); + this.healthDetailsHandler.addHealthDetails(builder, edition, resultSummary); + } } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java new file mode 100644 index 0000000000..77b8d51bcb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java @@ -0,0 +1,76 @@ +/* + * 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.neo4j; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.Driver; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.reactive.RxResult; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.retry.Retry; + +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; + +/** + * {@link ReactiveHealthIndicator} that tests the status of a Neo4j by executing a Cypher + * statement and extracting server and database information. + * + * @author Michael J. Simons + * @author Stephane Nicoll + * @since 2.4.0 + */ +public final class Neo4jReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + private static final Log logger = LogFactory.getLog(Neo4jReactiveHealthIndicator.class); + + private final Driver driver; + + private final Neo4jHealthDetailsHandler healthDetailsHandler; + + public Neo4jReactiveHealthIndicator(Driver driver) { + this.driver = driver; + this.healthDetailsHandler = new Neo4jHealthDetailsHandler(); + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + return runHealthCheckQuery() + .doOnError(SessionExpiredException.class, + (e) -> logger.warn(Neo4jHealthIndicator.MESSAGE_SESSION_EXPIRED)) + .retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance)).map((result) -> { + this.healthDetailsHandler.addHealthDetails(builder, result.getT1(), result.getT2()); + return builder.build(); + }); + } + + Mono> runHealthCheckQuery() { + // We use WRITE here to make sure UP is returned for a server that supports + // all possible workloads + return Mono.using(() -> this.driver.rxSession(Neo4jHealthIndicator.DEFAULT_SESSION_CONFIG), (session) -> { + RxResult result = session.run(Neo4jHealthIndicator.CYPHER); + return Mono.from(result.records()).map((record) -> record.get("edition").asString()) + .zipWhen((edition) -> Mono.from(result.consume())); + }, RxSession::close); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java index 340170eff5..7b89b60097 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java index 6eec0f5933..a72f1fa8fd 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java @@ -16,25 +16,29 @@ package org.springframework.boot.actuate.neo4j; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.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 org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.summary.ResultSummary; 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Tests for {@link Neo4jHealthIndicator}. @@ -45,46 +49,85 @@ import static org.mockito.Mockito.mock; */ class Neo4jHealthIndicatorTests { - private Session session; - - private Neo4jHealthIndicator neo4jHealthIndicator; - - @BeforeEach - void before() { - this.session = mock(Session.class); - SessionFactory sessionFactory = mock(SessionFactory.class); - given(sessionFactory.openSession()).willReturn(this.session); - this.neo4jHealthIndicator = new Neo4jHealthIndicator(sessionFactory); - } - @Test - void neo4jUp() { - Result result = mock(Result.class); - given(this.session.query(Neo4jHealthIndicator.CYPHER, Collections.emptyMap())).willReturn(result); - Map expectedCypherDetails = new HashMap<>(); - String edition = "community"; - String version = "4.0.0"; - expectedCypherDetails.put("edition", edition); - expectedCypherDetails.put("version", version); - List> queryResults = new ArrayList<>(); - queryResults.add(expectedCypherDetails); - given(result.queryResults()).willReturn(queryResults); - Health health = this.neo4jHealthIndicator.health(); + void neo4jIsUp() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", "test"); + Driver driver = mockDriver(resultSummary, "ultimate collectors edition"); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - Map details = health.getDetails(); - String editionFromDetails = details.get("edition").toString(); - String versionFromDetails = details.get("version").toString(); - assertThat(editionFromDetails).isEqualTo(edition); - assertThat(versionFromDetails).isEqualTo(version); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("database", "test"); + assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); } @Test - void neo4jDown() { - CypherException cypherException = new CypherException("Neo.ClientError.Statement.SyntaxError", - "Error executing Cypher"); - given(this.session.query(Neo4jHealthIndicator.CYPHER, Collections.emptyMap())).willThrow(cypherException); - Health health = this.neo4jHealthIndicator.health(); + void neo4jIsUpWithoutDatabaseName() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", null); + Driver driver = mockDriver(resultSummary, "some edition"); + Health health = new Neo4jHealthIndicator(driver).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).doesNotContainKey("database"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); + } + + @Test + void neo4jIsUpWithEmptyDatabaseName() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + Driver driver = mockDriver(resultSummary, "some edition"); + Health health = new Neo4jHealthIndicator(driver).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).doesNotContainKey("database"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); + } + + @Test + void neo4jIsUpWithOneSessionExpiredException() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + Session session = mock(Session.class); + Result statementResult = mockStatementResult(resultSummary, "some edition"); + AtomicInteger count = new AtomicInteger(0); + given(session.run(anyString())).will((invocation) -> { + if (count.compareAndSet(0, 1)) { + throw new SessionExpiredException("Session expired"); + } + return statementResult; + }); + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willReturn(session); + Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(driver); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + verify(session, times(2)).close(); + } + + @Test + void neo4jIsDown() { + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willThrow(ServiceUnavailableException.class); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).containsKeys("error"); + } + + private Result mockStatementResult(ResultSummary resultSummary, String edition) { + Record record = mock(Record.class); + given(record.get("edition")).willReturn(Values.value(edition)); + Result statementResult = mock(Result.class); + given(statementResult.single()).willReturn(record); + given(statementResult.consume()).willReturn(resultSummary); + return statementResult; + } + + private Driver mockDriver(ResultSummary resultSummary, String edition) { + Result statementResult = mockStatementResult(resultSummary, edition); + Session session = mock(Session.class); + given(session.run(anyString())).willReturn(statementResult); + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willReturn(session); + return driver; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java new file mode 100644 index 0000000000..9379d92d0f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTest.java @@ -0,0 +1,116 @@ +/* + * 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.neo4j; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.reactive.RxResult; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link Neo4jReactiveHealthIndicator}. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jReactiveHealthIndicatorTest { + + @Test + void neo4jIsUp() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", "test"); + Driver driver = mockDriver(resultSummary, "ultimate collectors edition"); + Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); + }).verifyComplete(); + } + + @Test + void neo4jIsUpWithOneSessionExpiredException() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + RxSession session = mock(RxSession.class); + RxResult statementResult = mockStatementResult(resultSummary, "some edition"); + AtomicInteger count = new AtomicInteger(0); + given(session.run(anyString())).will((invocation) -> { + if (count.compareAndSet(0, 1)) { + throw new SessionExpiredException("Session expired"); + } + return statementResult; + }); + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willReturn(session); + Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); + }).verifyComplete(); + verify(session, times(2)).close(); + } + + @Test + void neo4jIsDown() { + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willThrow(ServiceUnavailableException.class); + Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).containsKeys("error"); + }).verifyComplete(); + } + + private RxResult mockStatementResult(ResultSummary resultSummary, String edition) { + Record record = mock(Record.class); + given(record.get("edition")).willReturn(Values.value(edition)); + RxResult statementResult = mock(RxResult.class); + given(statementResult.records()).willReturn(Mono.just(record)); + given(statementResult.consume()).willReturn(Mono.just(resultSummary)); + return statementResult; + } + + private Driver mockDriver(ResultSummary resultSummary, String edition) { + RxResult statementResult = mockStatementResult(resultSummary, edition); + RxSession session = mock(RxSession.class); + given(session.run(anyString())).willReturn(statementResult); + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willReturn(session); + return driver; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java new file mode 100644 index 0000000000..4bbd715a0e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java @@ -0,0 +1,48 @@ +/* + * 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.neo4j; + +import org.neo4j.driver.summary.DatabaseInfo; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test utility to mock {@link ResultSummary}. + * + * @author Stephane Nicoll + */ +final class ResultSummaryMock { + + private ResultSummaryMock() { + } + + static ResultSummary createResultSummary(String serverVersion, String serverAddress, String databaseName) { + ServerInfo serverInfo = mock(ServerInfo.class); + given(serverInfo.version()).willReturn(serverVersion); + given(serverInfo.address()).willReturn(serverAddress); + DatabaseInfo databaseInfo = mock(DatabaseInfo.class); + given(databaseInfo.name()).willReturn(databaseName); + ResultSummary resultSummary = mock(ResultSummary.class); + given(resultSummary.server()).willReturn(serverInfo); + given(resultSummary.database()).willReturn(databaseInfo); + return resultSummary; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 5109c93106..b872595e9b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -856,6 +856,9 @@ The following `ReactiveHealthIndicators` are auto-configured by Spring Boot when | {spring-boot-actuator-module-code}/mongo/MongoReactiveHealthIndicator.java[`MongoReactiveHealthIndicator`] | Checks that a Mongo database is up. +| {spring-boot-actuator-module-code}/neo4j/Neo4jReactiveHealthIndicator.java[`Neo4jReactiveHealthIndicator`] +| Checks that a Neo4j database is up. + | {spring-boot-actuator-module-code}/redis/RedisReactiveHealthIndicator.java[`RedisReactiveHealthIndicator`] | Checks that a Redis server is up. |===