Merge pull request #22302 from meistermeier
* pr/22302: Polish "Update Neo4j health check to use the Neo4j Driver" Update Neo4j health check to use the Neo4j Driver Closes gh-22302
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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<Neo4jHealthIndicator, SessionFactory> {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" })
|
||||
public HealthContributor neo4jHealthContributor(Map<String, SessionFactory> sessionFactories) {
|
||||
return createContributor(sessionFactories);
|
||||
}
|
||||
@AutoConfigureAfter(Neo4jAutoConfiguration.class)
|
||||
@Import({ Neo4jReactiveConfiguration.class, Neo4jConfiguration.class })
|
||||
public class Neo4jHealthContributorAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Neo4jHealthIndicator, Driver> {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" })
|
||||
HealthContributor neo4jHealthContributor(Map<String, Driver> drivers) {
|
||||
return createContributor(drivers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(Flux.class)
|
||||
static class Neo4jReactiveConfiguration
|
||||
extends CompositeReactiveHealthContributorConfiguration<Neo4jReactiveHealthIndicator, Driver> {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" })
|
||||
ReactiveHealthContributor neo4jHealthContributor(Map<String, Driver> drivers) {
|
||||
return createContributor(drivers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Health> 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<Tuple2<String, ResultSummary>> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,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<String, Object> expectedCypherDetails = new HashMap<>();
|
||||
String edition = "community";
|
||||
String version = "4.0.0";
|
||||
expectedCypherDetails.put("edition", edition);
|
||||
expectedCypherDetails.put("version", version);
|
||||
List<Map<String, Object>> 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<String, Object> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|===
|
||||
|
||||
Reference in New Issue
Block a user