Update Neo4j health check to use the Neo4j Driver

This commit replaces the Neo4j-OGM based health checks with one based on
the Neo4j Java driver. A Reactive variant is also added in this commit.

See gh-22302
pull/22631/head
Gerrit Meier 4 years ago committed by Stephane Nicoll
parent 2756f5911f
commit c5a7815e42

@ -0,0 +1,78 @@
/*
* 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;
});
});
}
}

@ -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,77 @@
package org.springframework.boot.actuate.autoconfigure.neo4j;
import java.util.Map;
import org.neo4j.ogm.session.SessionFactory;
import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration;
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.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.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Flux;
import java.util.Map;
import org.neo4j.driver.Driver;
/**
* {@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.
*
* @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, Health.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);
@AutoConfigureBefore(HealthContributorAutoConfiguration.class)
@AutoConfigureAfter({ Neo4jDriverAutoConfiguration.class, Neo4jDataAutoConfiguration.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);
}
}
}

@ -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.

@ -0,0 +1,141 @@
/*
* 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;
}
}
}

@ -0,0 +1,62 @@
/*
* 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,11 +1,14 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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
* 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,
@ -13,79 +16,147 @@
* 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.Nested;
import org.junit.jupiter.api.Test;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
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;
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}.
*
* @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));
@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 runShouldCreateIndicator() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Neo4jHealthIndicator.class));
}
@Test
void runWhenDisabledShouldNotCreateIndicator() {
this.contextRunner.withPropertyValues("management.health.neo4j.enabled:false")
.run((context) -> assertThat(context).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 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));
});
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);
});
}
);
}
@Configuration(proxyBeanMethods = false)
static class Neo4jConfiguration {
static class WithDriver {
@Bean
SessionFactory sessionFactory() {
return mock(SessionFactory.class);
Driver driver() {
return mockDriverWithoutMetrics();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomIndicatorConfiguration {
static class WithCustomIndicator {
@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) throws Exception {
builder.up().withDetail("test", true);
}
};
}

@ -0,0 +1,124 @@
/*
* 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;
}
}
}

@ -16,61 +16,117 @@
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.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.Health.Builder;
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
* 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";
static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition";
private final SessionFactory sessionFactory;
/**
* Message indicating that the health check failed.
*/
static final String MESSAGE_HEALTH_CHECK_FAILED = "Neo4j health check failed";
/**
* 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();
/**
* The driver for this health indicator instance.
*/
private final Driver driver;
public Neo4jHealthIndicator(Driver driver) {
super(MESSAGE_HEALTH_CHECK_FAILED);
this.driver = driver;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Session session = this.sessionFactory.openSession();
extractResult(session, builder);
protected void doHealthCheck(Health.Builder builder) {
try {
ResultSummaryWithEdition resultSummaryWithEdition;
// Retry one time when the session has been expired
try {
resultSummaryWithEdition = runHealthCheckQuery();
}
catch (SessionExpiredException sessionExpiredException) {
logger.warn(MESSAGE_SESSION_EXPIRED);
resultSummaryWithEdition = runHealthCheckQuery();
}
buildStatusUp(resultSummaryWithEdition, 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
* 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
*/
protected void extractResult(Session session, Health.Builder builder) throws Exception {
Result result = session.query(CYPHER, Collections.emptyMap());
builder.up().withDetails(result.queryResults().iterator().next());
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() {
// 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);
}
}
}

@ -0,0 +1,71 @@
/*
* 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 reactor.core.publisher.Mono;
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
* @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;
public Neo4jReactiveHealthIndicator(Driver driver) {
this.driver = driver;
}
@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((r) -> Neo4jHealthIndicator.buildStatusUp(r, builder).build());
}
Mono<ResultSummaryWithEdition> 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));
}, RxSession::close);
}
}

@ -0,0 +1,37 @@
/*
* 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,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.

@ -0,0 +1,66 @@
/*
* 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();
}
}

@ -0,0 +1,62 @@
/*
* 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() {
}
}

@ -0,0 +1,64 @@
/*
* 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,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 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.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.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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
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}.
@ -43,48 +47,126 @@ import static org.mockito.Mockito.mock;
* @author Stephane Nicoll
* @author Michael Simons
*/
class Neo4jHealthIndicatorTests {
@ExtendWith(MockitoExtension.class)
class Neo4jHealthIndicatorTests extends Neo4jHealthIndicatorTestBase {
@Mock
private Session session;
private Neo4jHealthIndicator neo4jHealthIndicator;
@Mock
private Result statementResult;
@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();
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");
}
@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();
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");
}
@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();
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");
@BeforeEach
void before() {
this.session = mock(Session.class);
SessionFactory sessionFactory = mock(SessionFactory.class);
given(sessionFactory.openSession()).willReturn(this.session);
this.neo4jHealthIndicator = new Neo4jHealthIndicator(sessionFactory);
verify(session).close();
verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo,
this.databaseInfo);
}
@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 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)) {
throw new SessionExpiredException("Session expired");
}
return Neo4jHealthIndicatorTests.this.statementResult;
});
when(driver.session(any(SessionConfig.class))).thenReturn(this.session);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(this.driver);
Health health = healthIndicator.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@Zu Hause");
verify(this.session, times(2)).close();
verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo,
this.databaseInfo);
}
@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 neo4jSessionIsDown() {
when(driver.session(any(SessionConfig.class))).thenThrow(ServiceUnavailableException.class);
Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(driver);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).containsKeys("error");
verifyNoMoreInteractions(this.driver, this.session, this.statementResult, this.resultSummary, this.serverInfo,
this.databaseInfo);
}
}

@ -0,0 +1,117 @@
/*
* 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.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.neo4j.driver.SessionConfig;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
/**
* @author Michael J. Simons
*/
@ExtendWith(MockitoExtension.class)
class Neo4jReactiveHealthIndicatorTest extends Neo4jHealthIndicatorTestBase {
@Mock
private RxSession session;
@Mock
private RxResult statementResult;
@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);
Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver);
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");
}).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)) {
throw new SessionExpiredException("Session expired");
}
return statementResult;
});
when(driver.rxSession(any(SessionConfig.class))).thenReturn(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@Zu Hause");
assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors 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);
Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver);
healthIndicator.health().as(StepVerifier::create).consumeNextWith(health -> {
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).containsKeys("error");
}).verifyComplete();
verifyNoMoreInteractions(driver, session, statementResult, resultSummary, serverInfo, databaseInfo);
}
}
Loading…
Cancel
Save