Polish "Update Neo4j health check to use the Neo4j Driver"

See gh-22302
pull/22631/head
Stephane Nicoll 4 years ago
parent c5a7815e42
commit c6fde1e4d5

@ -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,78 +0,0 @@
/*
* 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 io.micrometer.core.instrument.MeterRegistry;
import java.util.Collections;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.neo4j.driver.Driver;
import org.springframework.boot.actuate.neo4j.Neo4jDriverMetrics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
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.neo4j.Neo4jDriverAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for metrics on all available
* {@link Driver drivers}.
* <p>
* The reason we are doing this dance with the manual binding is the fact that this
* autoconfiguration should work with more than one instance of the driver. If a user has
* multiple instances configured, than each instance should be bound via the binder to
* registry. Without that requirement, we could just add a {@link Bean @Bean} of type
* {@link Neo4jDriverMetrics} to the context and be done.
*
* @author Michael J. Simons
* @since 2.4.0
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({ MetricsAutoConfiguration.class, Neo4jDriverAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class })
@ConditionalOnClass({ Driver.class, MeterRegistry.class })
@ConditionalOnBean({ Driver.class, MeterRegistry.class })
public class Neo4jDriverMetricsAutoConfiguration {
private static final Log logger = LogFactory.getLog(Neo4jDriverMetricsAutoConfiguration.class);
@Autowired
public void bindDataSourcesToRegistry(Map<String, Driver> drivers, MeterRegistry registry) {
drivers.forEach((name, driver) -> {
if (!Neo4jDriverMetrics.metricsAreEnabled(driver)) {
return;
}
driver.verifyConnectivityAsync()
.thenRunAsync(() -> new Neo4jDriverMetrics(name, driver, Collections.emptyList()).bindTo(registry))
.exceptionally(e -> {
logger.warn("Could not verify connection for " + driver + " and thus not bind to metrics: "
+ e.getMessage());
return null;
});
});
}
}

@ -16,36 +16,24 @@
package org.springframework.boot.actuate.autoconfigure.neo4j;
import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration;
import org.neo4j.driver.Driver;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.ReactiveHealthContributor;
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.AutoConfigureBefore;
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.boot.autoconfigure.neo4j.Neo4jDriverAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Flux;
import java.util.Map;
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link Neo4jHealthIndicator}.
* The auto-configuration here is responsible for both imperative and reactive health
* checks. The reactive health check has precedence over the imperative one.
* {@link EnableAutoConfiguration Auto-configuration} for
* {@link Neo4jReactiveHealthIndicator} and {@link Neo4jHealthIndicator}.
*
* @author Eric Spiegelberg
* @author Stephane Nicoll
@ -53,40 +41,11 @@ import org.neo4j.driver.Driver;
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Driver.class, Health.class })
@ConditionalOnClass(Driver.class)
@ConditionalOnBean(Driver.class)
@ConditionalOnEnabledHealthIndicator("neo4j")
@AutoConfigureBefore(HealthContributorAutoConfiguration.class)
@AutoConfigureAfter({ Neo4jDriverAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@AutoConfigureAfter(Neo4jAutoConfiguration.class)
@Import({ Neo4jReactiveConfiguration.class, Neo4jConfiguration.class })
public class Neo4jHealthContributorAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Order(-20)
static class Neo4jHealthIndicatorConfiguration
extends CompositeHealthContributorConfiguration<Neo4jHealthIndicator, Driver> {
@Bean
// If Neo4jReactiveHealthIndicatorConfiguration kicked in, don't add the
// imperative version as well
@ConditionalOnMissingBean(name = "neo4jHealthContributor")
public HealthContributor neo4jHealthContributor(Map<String, Driver> drivers) {
return createContributor(drivers);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Flux.class })
@Order(-30)
static class Neo4jReactiveHealthIndicatorConfiguration
extends CompositeReactiveHealthContributorConfiguration<Neo4jReactiveHealthIndicator, Driver> {
@Bean
@ConditionalOnMissingBean(name = "neo4jHealthContributor")
public ReactiveHealthContributor neo4jHealthContributor(Map<String, Driver> drivers) {
return createComposite(drivers);
}
}
}

@ -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);
}
}
}

@ -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,141 +0,0 @@
/*
* 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 io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.neo4j.Neo4jDriverMetrics;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.neo4j.driver.Driver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jDriverMocks.mockDriverWithMetrics;
import static org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jDriverMocks.mockDriverWithoutMetrics;
/**
* @author Michael J. Simons
*/
class Neo4jDriverMetricsAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(MetricsAutoConfiguration.class, Neo4jDriverMetricsAutoConfiguration.class));
private final ContextConsumer<AssertableApplicationContext> assertNoInteractionsWithRegistry = ctx -> {
if (ctx.getBeansOfType(MeterRegistry.class).isEmpty()) {
return;
}
MeterRegistry mockedRegistry = ctx.getBean(MeterRegistry.class);
verify(mockedRegistry).config();
verifyNoMoreInteractions(mockedRegistry);
};
@Nested
class NoMatches {
@Test
void shouldRequireAllNeededClasses() {
contextRunner.withUserConfiguration(WithMeterRegistry.class)
.withClassLoader(new FilteredClassLoader(Driver.class)).run(assertNoInteractionsWithRegistry);
contextRunner.withUserConfiguration(WithDriverWithMetrics.class)
.withClassLoader(new FilteredClassLoader(MeterRegistry.class))
.run(assertNoInteractionsWithRegistry);
}
@Test
void shouldRequireAllNeededBeans() {
contextRunner.withUserConfiguration(WithDriverWithMetrics.class).run(assertNoInteractionsWithRegistry);
contextRunner.withUserConfiguration(WithMeterRegistry.class).run(assertNoInteractionsWithRegistry);
}
@Test
void shouldRequireDriverWithMetrics() {
contextRunner.withUserConfiguration(WithDriverWithoutMetrics.class, WithMeterRegistry.class)
.run(assertNoInteractionsWithRegistry);
}
}
@Nested
class Matches {
@Test
void shouldRequireDriverWithMetrics() {
contextRunner.withUserConfiguration(WithDriverWithMetrics.class, WithMeterRegistry.class).run(ctx -> {
// Wait a bit to let the completable future of the test that mocks
// connectiviy complete.
Thread.sleep(500L);
MeterRegistry meterRegistry = ctx.getBean(MeterRegistry.class);
assertThat(meterRegistry.getMeters()).extracting(m -> m.getId().getName())
.filteredOn(s -> s.startsWith(Neo4jDriverMetrics.PREFIX)).isNotEmpty();
});
}
}
@Configuration(proxyBeanMethods = false)
static class WithDriverWithMetrics {
@Bean
Driver driver() {
return mockDriverWithMetrics();
}
}
@Configuration(proxyBeanMethods = false)
static class WithDriverWithoutMetrics {
@Bean
Driver driver() {
return mockDriverWithoutMetrics();
}
}
@Configuration(proxyBeanMethods = false)
static class WithMeterRegistry {
@Bean
MeterRegistry meterRegistry() {
MeterRegistry meterRegistry = spy(SimpleMeterRegistry.class);
return meterRegistry;
}
}
}

@ -1,62 +0,0 @@
/*
* 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.Collections;
import java.util.concurrent.CompletableFuture;
import org.neo4j.driver.ConnectionPoolMetrics;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Metrics;
import org.neo4j.driver.exceptions.ClientException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Some predefined mocks, only to be used internally for tests.
*
* @author Michael J. Simons
*/
final class Neo4jDriverMocks {
public static Driver mockDriverWithMetrics() {
ConnectionPoolMetrics p1 = mock(ConnectionPoolMetrics.class);
when(p1.id()).thenReturn("p1");
Metrics metrics = mock(Metrics.class);
when(metrics.connectionPoolMetrics()).thenReturn(Collections.singletonList(p1));
Driver driver = mock(Driver.class);
when(driver.metrics()).thenReturn(metrics);
when(driver.verifyConnectivityAsync()).thenReturn(CompletableFuture.completedFuture(null));
return driver;
}
public static Driver mockDriverWithoutMetrics() {
Driver driver = mock(Driver.class);
when(driver.metrics()).thenThrow(ClientException.class);
return driver;
}
private Neo4jDriverMocks() {
}
}

@ -1,14 +1,11 @@
/*
* Copyright (c) 2019-2020 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
* 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
* 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,
@ -18,14 +15,14 @@
*/
package org.springframework.boot.actuate.autoconfigure.neo4j;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
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.CompositeReactiveHealthContributor;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.PingHealthIndicator;
import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator;
import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@ -33,14 +30,10 @@ 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;
import reactor.core.publisher.Flux;
import org.neo4j.driver.Driver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
import static org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jDriverMocks.mockDriverWithoutMetrics;
/**
* Tests for {@link Neo4jHealthContributorAutoConfiguration}.
@ -55,106 +48,61 @@ class Neo4jHealthContributorAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
Neo4jHealthContributorAutoConfiguration.class));
@Nested
class NoMatches {
@Test
void shouldRespectManagementDecisions() {
contextRunner.withUserConfiguration(WithDriver.class)
.withPropertyValues("management.health.neo4j.enabled=false")
.run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class)
.hasSingleBean(PingHealthIndicator.class));
}
@Test
void shouldRequireHealthClass() {
contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Health.class))
.run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
}
@Test
void shouldRequireDriverClass() {
contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Driver.class))
.run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
}
@Test
void shouldRequireDriverBean() {
contextRunner.run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
}
@Test
void shouldRequireHealthIndicatorClasses() {
contextRunner.withUserConfiguration(WithDriver.class)
.withClassLoader(new FilteredClassLoader(Health.class, Flux.class))
.run(ctx -> assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Flux.class))
.run(ctx -> assertThat(ctx).hasSingleBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
}
@Test
void defaultIndicatorCanBeReplaced() {
contextRunner.withUserConfiguration(WithDriver.class, WithCustomIndicator.class).run((context) -> {
assertThat(context).hasBean("neo4jHealthIndicator");
Health health = context.getBean("neo4jHealthIndicator", HealthIndicator.class).health();
assertThat(health.getDetails()).containsOnly(entry("test", true));
});
}
@Test
void runShouldCreateHealthIndicator() {
this.contextRunner.withUserConfiguration(Neo4jConfiguration.class).run((context) -> assertThat(context)
.hasSingleBean(Neo4jReactiveHealthIndicator.class).doesNotHaveBean(Neo4jHealthIndicator.class));
}
@Nested
class Matches {
@Test
void shouldCreateHealthIndicator() {
contextRunner.withUserConfiguration(WithDriver.class).withClassLoader(new FilteredClassLoader(Flux.class))
.run(ctx -> assertThat(ctx).hasSingleBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.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.withUserConfiguration(Neo4jConfiguration.class)
.withPropertyValues("management.health.neo4j.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class)
.doesNotHaveBean(Neo4jReactiveHealthIndicator.class));
}
@Test
void reactiveHealthCheckShouldHavePrecedence() {
contextRunner.withUserConfiguration(WithDriver.class).run(ctx -> {
assertThat(ctx).doesNotHaveBean(Neo4jHealthIndicator.class).hasBean("neo4jHealthContributor");
assertThat(ctx).getBean("neo4jHealthContributor").isInstanceOf(CompositeReactiveHealthContributor.class)
.satisfies(o -> {
CompositeReactiveHealthContributor chc = (CompositeReactiveHealthContributor) o;
assertThat(chc.getContributor("driver")).isInstanceOf(Neo4jReactiveHealthIndicator.class);
});
}
void defaultIndicatorCanBeReplaced() {
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 WithDriver {
static class Neo4jConfiguration {
@Bean
Driver driver() {
return mockDriverWithoutMetrics();
return mock(Driver.class);
}
}
@Configuration(proxyBeanMethods = false)
static class WithCustomIndicator {
static class CustomIndicatorConfiguration {
@Bean
HealthIndicator neo4jHealthIndicator() {
return new AbstractHealthIndicator() {
protected void doHealthCheck(Health.Builder builder) throws Exception {
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")

@ -1,124 +0,0 @@
/*
* 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.function.Consumer;
import io.micrometer.core.instrument.FunctionCounter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.neo4j.driver.ConnectionPoolMetrics;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Metrics;
import org.neo4j.driver.exceptions.ClientException;
import org.springframework.util.Assert;
/**
* This is a {@link MeterBinder} that binds all available Neo4j driver metrics to
* Micrometer.
*
* @author Michael J. Simons
* @since 2.4.0
*/
public final class Neo4jDriverMetrics implements MeterBinder {
/**
* Prefixed used for driver metrics.
*/
public static final String PREFIX = "neo4j.driver.connections";
private static final String BASE_UNIT_CONNECTIONS = "connections";
private final Driver driver;
private final Iterable<Tag> tags;
public Neo4jDriverMetrics(String name, Driver driver, Iterable<Tag> tags) {
Assert.notNull(name, "Bean name must not be null");
Assert.notNull(driver, "Driver must not be null");
Assert.notNull(tags, "Tags must not be null (but may be empty)");
this.driver = driver;
this.tags = Tags.concat(tags, "name", name);
}
@Override
public void bindTo(MeterRegistry meterRegistry) {
Metrics metrics = this.driver.metrics();
metrics.connectionPoolMetrics().forEach(this.getPoolMetricsBinder(meterRegistry));
}
Consumer<ConnectionPoolMetrics> getPoolMetricsBinder(MeterRegistry meterRegistry) {
return (poolMetrics) -> {
Iterable<Tag> poolTags = Tags.concat(this.tags, "poolId", poolMetrics.id());
FunctionCounter.builder(PREFIX + ".acquired", poolMetrics, ConnectionPoolMetrics::acquired).tags(poolTags)
.baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections that have been acquired.")
.register(meterRegistry);
FunctionCounter.builder(PREFIX + ".closed", poolMetrics, ConnectionPoolMetrics::closed).tags(poolTags)
.baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections have been closed.")
.register(meterRegistry);
FunctionCounter.builder(PREFIX + ".created", poolMetrics, ConnectionPoolMetrics::created).tags(poolTags)
.baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections have ever been created.")
.register(meterRegistry);
FunctionCounter.builder(PREFIX + ".failedToCreate", poolMetrics, ConnectionPoolMetrics::failedToCreate)
.tags(poolTags).baseUnit(BASE_UNIT_CONNECTIONS)
.description("The amount of connections have been failed to create.").register(meterRegistry);
Gauge.builder(PREFIX + ".idle", poolMetrics, ConnectionPoolMetrics::idle).tags(poolTags)
.baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections that are currently idle.")
.register(meterRegistry);
Gauge.builder(PREFIX + ".inUse", poolMetrics, ConnectionPoolMetrics::inUse).tags(poolTags)
.baseUnit(BASE_UNIT_CONNECTIONS).description("The amount of connections that are currently in-use.")
.register(meterRegistry);
FunctionCounter
.builder(PREFIX + ".timedOutToAcquire", poolMetrics, ConnectionPoolMetrics::timedOutToAcquire)
.tags(poolTags).baseUnit(BASE_UNIT_CONNECTIONS)
.description(
"The amount of failures to acquire a connection from a pool within maximum connection acquisition timeout.")
.register(meterRegistry);
};
}
/**
* Utility method to check whether driver metrics are enabled without throwing an
* exception.
* @param driver the bean to check whether it has metrics enabled or not
* @return true, if the given bean exposes metrics
*/
public static boolean metricsAreEnabled(Driver driver) {
try {
driver.metrics();
return true;
}
catch (ClientException ex) {
return false;
}
}
}

@ -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());
}
}
}

@ -24,14 +24,11 @@ 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.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.util.StringUtils;
/**
* {@link HealthIndicator} that tests the status of a Neo4j by executing a Cypher
@ -51,11 +48,6 @@ public class Neo4jHealthIndicator extends AbstractHealthIndicator {
*/
static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
/**
* Message indicating that the health check failed.
*/
static final String MESSAGE_HEALTH_CHECK_FAILED = "Neo4j health check failed";
/**
* Message logged before retrying a health check.
*/
@ -67,65 +59,41 @@ public class Neo4jHealthIndicator extends AbstractHealthIndicator {
static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder().withDefaultAccessMode(AccessMode.WRITE)
.build();
/**
* The driver for this health indicator instance.
*/
private final Driver driver;
private final Neo4jHealthDetailsHandler healthDetailsHandler;
public Neo4jHealthIndicator(Driver driver) {
super(MESSAGE_HEALTH_CHECK_FAILED);
super("Neo4j health check failed");
this.driver = driver;
this.healthDetailsHandler = new Neo4jHealthDetailsHandler();
}
@Override
protected void doHealthCheck(Health.Builder builder) {
try {
ResultSummaryWithEdition resultSummaryWithEdition;
// Retry one time when the session has been expired
try {
resultSummaryWithEdition = runHealthCheckQuery();
runHealthCheckQuery(builder);
}
catch (SessionExpiredException sessionExpiredException) {
catch (SessionExpiredException ex) {
// Retry one time when the session has been expired
logger.warn(MESSAGE_SESSION_EXPIRED);
resultSummaryWithEdition = runHealthCheckQuery();
runHealthCheckQuery(builder);
}
buildStatusUp(resultSummaryWithEdition, builder);
}
catch (Exception ex) {
builder.down().withException(ex);
}
}
/**
* Applies the given {@link ResultSummary} to the {@link Health.Builder builder}
* without actually calling {@code build}.
* @param resultSummaryWithEdition the result summary returned by the server
* @param builder the health builder to be modified
* @return the modified health builder
*/
static Health.Builder buildStatusUp(ResultSummaryWithEdition resultSummaryWithEdition, Health.Builder builder) {
ServerInfo serverInfo = resultSummaryWithEdition.resultSummary.server();
DatabaseInfo databaseInfo = resultSummaryWithEdition.resultSummary.database();
builder.up().withDetail("server", serverInfo.version() + "@" + serverInfo.address()).withDetail("edition",
resultSummaryWithEdition.edition);
if (StringUtils.hasText(databaseInfo.name())) {
builder.withDetail("database", databaseInfo.name());
}
return builder;
}
ResultSummaryWithEdition runHealthCheckQuery() {
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();
return new ResultSummaryWithEdition(resultSummary, edition);
this.healthDetailsHandler.addHealthDetails(builder, edition, resultSummary);
}
}

@ -22,7 +22,9 @@ 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;
@ -34,19 +36,20 @@ import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
* 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);
/**
* The driver for this health indicator instance.
*/
private final Driver driver;
private final Neo4jHealthDetailsHandler healthDetailsHandler;
public Neo4jReactiveHealthIndicator(Driver driver) {
this.driver = driver;
this.healthDetailsHandler = new Neo4jHealthDetailsHandler();
}
@Override
@ -54,17 +57,19 @@ public final class Neo4jReactiveHealthIndicator extends AbstractReactiveHealthIn
return runHealthCheckQuery()
.doOnError(SessionExpiredException.class,
(e) -> logger.warn(Neo4jHealthIndicator.MESSAGE_SESSION_EXPIRED))
.retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance))
.map((r) -> Neo4jHealthIndicator.buildStatusUp(r, builder).build());
.retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance)).map((result) -> {
this.healthDetailsHandler.addHealthDetails(builder, result.getT1(), result.getT2());
return builder.build();
});
}
Mono<ResultSummaryWithEdition> runHealthCheckQuery() {
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()), (e, r) -> new ResultSummaryWithEdition(r, e));
.zipWhen((edition) -> Mono.from(result.consume()));
}, RxSession::close);
}

@ -1,37 +0,0 @@
/*
* 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.ResultSummary;
/**
* A holder for a {@link ResultSummary result summary} and database edition.
*
* @author Michael J. Simons
*/
final class ResultSummaryWithEdition {
final ResultSummary resultSummary;
final String edition;
ResultSummaryWithEdition(ResultSummary resultSummary, String edition) {
this.resultSummary = resultSummary;
this.edition = edition;
}
}

@ -1,66 +0,0 @@
/*
* 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 io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import org.neo4j.driver.Driver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.actuate.neo4j.Neo4jDriverMetrics.PREFIX;
import static org.springframework.boot.actuate.neo4j.Neo4jDriverMocks.mockDriverWithMetrics;
import static org.springframework.boot.actuate.neo4j.Neo4jDriverMocks.mockDriverWithoutMetrics;
/**
* @author Michael J. Simons
*/
class Neo4jDriverMetricsTests {
@Test
void shouldDetectEnabledMetrics() {
Driver driver = mockDriverWithMetrics();
assertThat(Neo4jDriverMetrics.metricsAreEnabled(driver)).isTrue();
}
@Test
void shouldDetectDisabledMetrics() {
Driver driver = mockDriverWithoutMetrics();
assertThat(Neo4jDriverMetrics.metricsAreEnabled(driver)).isFalse();
}
@Test
void shouldRegisterCorrectMeters() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();
Neo4jDriverMetrics metrics = new Neo4jDriverMetrics("driver", mockDriverWithMetrics(), Collections.emptyList());
metrics.bindTo(registry);
assertThat(registry.get(PREFIX + ".acquired").functionCounter()).isNotNull();
assertThat(registry.get(PREFIX + ".closed").functionCounter()).isNotNull();
assertThat(registry.get(PREFIX + ".created").functionCounter()).isNotNull();
assertThat(registry.get(PREFIX + ".failedToCreate").functionCounter()).isNotNull();
assertThat(registry.get(PREFIX + ".idle").gauge()).isNotNull();
assertThat(registry.get(PREFIX + ".inUse").gauge()).isNotNull();
assertThat(registry.get(PREFIX + ".timedOutToAcquire").functionCounter()).isNotNull();
}
}

@ -1,62 +0,0 @@
/*
* 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.Collections;
import java.util.concurrent.CompletableFuture;
import org.neo4j.driver.ConnectionPoolMetrics;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Metrics;
import org.neo4j.driver.exceptions.ClientException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Some predefined mocks, only to be used internally for tests.
*
* @author Michael J. Simons
*/
final class Neo4jDriverMocks {
public static Driver mockDriverWithMetrics() {
ConnectionPoolMetrics p1 = mock(ConnectionPoolMetrics.class);
when(p1.id()).thenReturn("p1");
Metrics metrics = mock(Metrics.class);
when(metrics.connectionPoolMetrics()).thenReturn(Collections.singletonList(p1));
Driver driver = mock(Driver.class);
when(driver.metrics()).thenReturn(metrics);
when(driver.verifyConnectivityAsync()).thenReturn(CompletableFuture.completedFuture(null));
return driver;
}
public static Driver mockDriverWithoutMetrics() {
Driver driver = mock(Driver.class);
when(driver.metrics()).thenThrow(ClientException.class);
return driver;
}
private Neo4jDriverMocks() {
}
}

@ -1,64 +0,0 @@
/*
* Copyright (c) 2019-2020 "Neo4j,"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.mockito.Mock;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Record;
import org.neo4j.driver.Values;
import org.neo4j.driver.summary.DatabaseInfo;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.summary.ServerInfo;
import static org.mockito.Mockito.when;
/**
* Contains some shared mocks for the health indicator tests.
*
* @author Michael J. Simons
*/
abstract class Neo4jHealthIndicatorTestBase {
@Mock
protected Driver driver;
@Mock
protected ResultSummary resultSummary;
@Mock
protected ServerInfo serverInfo;
@Mock
protected DatabaseInfo databaseInfo;
@Mock
protected Record record;
protected void prepareSharedMocks() {
when(serverInfo.version()).thenReturn("4711");
when(serverInfo.address()).thenReturn("Zu Hause");
when(databaseInfo.name()).thenReturn("n/a");
when(resultSummary.server()).thenReturn(serverInfo);
when(resultSummary.database()).thenReturn(databaseInfo);
when(record.get("edition")).thenReturn(Values.value("ultimate collectors edition"));
}
}

@ -16,29 +16,29 @@
package org.springframework.boot.actuate.neo4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
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.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;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link Neo4jHealthIndicator}.
@ -47,126 +47,87 @@ import static org.mockito.Mockito.when;
* @author Stephane Nicoll
* @author Michael Simons
*/
@ExtendWith(MockitoExtension.class)
class Neo4jHealthIndicatorTests extends Neo4jHealthIndicatorTestBase {
@Mock
private Session session;
@Mock
private Result statementResult;
class Neo4jHealthIndicatorTests {
@Test
void shouldWorkWithoutDatabaseName() {
when(this.serverInfo.version()).thenReturn("4711");
when(this.serverInfo.address()).thenReturn("Zu Hause");
when(this.resultSummary.server()).thenReturn(this.serverInfo);
when(this.resultSummary.database()).thenReturn(this.databaseInfo);
when(this.databaseInfo.name()).thenReturn(null);
when(record.get("edition")).thenReturn(Values.value("some edition"));
when(this.statementResult.single()).thenReturn(this.record);
when(this.statementResult.consume()).thenReturn(this.resultSummary);
when(this.session.run(anyString())).thenReturn(this.statementResult);
when(this.driver.session(any(SessionConfig.class))).thenReturn(this.session);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver);
Health health = healthIndicator.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);
assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause");
assertThat(health.getDetails()).doesNotContainKey("database");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).containsEntry("database", "test");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition");
}
@Test
void shouldWorkWithEmptyDatabaseName() {
when(this.serverInfo.version()).thenReturn("4711");
when(this.serverInfo.address()).thenReturn("Zu Hause");
when(this.resultSummary.server()).thenReturn(this.serverInfo);
when(this.resultSummary.database()).thenReturn(this.databaseInfo);
when(this.databaseInfo.name()).thenReturn("");
when(record.get("edition")).thenReturn(Values.value("some edition"));
when(this.statementResult.single()).thenReturn(this.record);
when(this.statementResult.consume()).thenReturn(this.resultSummary);
when(this.session.run(anyString())).thenReturn(this.statementResult);
when(driver.session(any(SessionConfig.class))).thenReturn(this.session);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver);
Health health = healthIndicator.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@Zu Hause");
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).doesNotContainKey("database");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
}
@Test
void neo4jIsUp() {
prepareSharedMocks();
when(this.statementResult.single()).thenReturn(this.record);
when(this.statementResult.consume()).thenReturn(this.resultSummary);
when(this.session.run(anyString())).thenReturn(this.statementResult);
when(this.driver.session(any(SessionConfig.class))).thenReturn(this.session);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver);
Health health = healthIndicator.health();
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@Zu Hause");
assertThat(health.getDetails()).containsEntry("database", "n/a");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition");
verify(session).close();
verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo,
this.databaseInfo);
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).doesNotContainKey("database");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
}
@Test
void neo4jSessionIsExpiredOnce() {
AtomicInteger cnt = new AtomicInteger(0);
prepareSharedMocks();
when(this.statementResult.single()).thenReturn(this.record);
when(this.statementResult.consume()).thenReturn(this.resultSummary);
when(this.session.run(anyString())).thenAnswer(invocation -> {
if (cnt.compareAndSet(0, 1)) {
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 Neo4jHealthIndicatorTests.this.statementResult;
return statementResult;
});
when(driver.session(any(SessionConfig.class))).thenReturn(this.session);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver);
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@Zu Hause");
verify(this.session, times(2)).close();
verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo,
this.databaseInfo);
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
verify(session, times(2)).close();
}
@Test
void neo4jSessionIsDown() {
when(driver.session(any(SessionConfig.class))).thenThrow(ServiceUnavailableException.class);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(driver);
Health health = healthIndicator.health();
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;
}
verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo,
this.databaseInfo);
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;
}
}

@ -16,102 +16,101 @@
package org.springframework.boot.actuate.neo4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.actuate.health.Status;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
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;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link Neo4jReactiveHealthIndicator}.
*
* @author Michael J. Simons
* @author Stephane Nicoll
*/
@ExtendWith(MockitoExtension.class)
class Neo4jReactiveHealthIndicatorTest extends Neo4jHealthIndicatorTestBase {
@Mock
private RxSession session;
@Mock
private RxResult statementResult;
class Neo4jReactiveHealthIndicatorTest {
@Test
void neo4jIsUp() {
prepareSharedMocks();
when(statementResult.records()).thenReturn(Mono.just(record));
when(statementResult.consume()).thenReturn(Mono.just(resultSummary));
when(session.run(anyString())).thenReturn(statementResult);
when(driver.rxSession(any(SessionConfig.class))).thenReturn(session);
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 -> {
healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> {
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause");
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition");
}).verifyComplete();
verify(session).close();
verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo);
}
@Test
void neo4jSessionIsExpiredOnce() {
AtomicInteger cnt = new AtomicInteger(0);
prepareSharedMocks();
when(statementResult.records()).thenReturn(Mono.just(record));
when(statementResult.consume()).thenReturn(Mono.just(resultSummary));
when(session.run(anyString())).thenAnswer(invocation -> {
if (cnt.compareAndSet(0, 1)) {
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;
});
when(driver.rxSession(any(SessionConfig.class))).thenReturn(session);
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 -> {
healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> {
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("server", "4711@Zu Hause");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition");
assertThat(health.getDetails()).containsEntry("server", "4711@My Home");
assertThat(health.getDetails()).containsEntry("edition", "some edition");
}).verifyComplete();
verify(session, times(2)).close();
verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo);
}
@Test
void neo4jSessionIsDown() {
when(driver.rxSession(any(SessionConfig.class))).thenThrow(ServiceUnavailableException.class);
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 -> {
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;
}
verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo);
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.
|===

Loading…
Cancel
Save