From 4ea0e0060b03a0fc1c0f7248b94ed2e0257561ea Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 6 May 2019 11:52:00 +0200 Subject: [PATCH] Add support for health indicator groups This commit allows a user to define an arbitrary number of health indicator groups using configuration. If a given health indicator is defined in more than one group, a single invocation is ensured using a AggregateHealth. A reactive counter-part is also provided. See gh-14022 --- .../health/HealthEndpointConfiguration.java | 4 +- ...althEndpointWebExtensionConfiguration.java | 13 +- .../HealthIndicatorAutoConfiguration.java | 48 ++++++- .../health/HealthIndicatorProperties.java | 52 ++++--- .../boot/actuate/health/AggregatedHealth.java | 68 +++++++++ .../health/AggregatedHealthIndicator.java | 41 ++++++ .../AggregatedReactiveHealthIndicator.java | 44 ++++++ .../actuate/health/GroupHealthIndicator.java | 68 +++++++++ .../health/GroupReactiveHealthIndicator.java | 65 +++++++++ .../boot/actuate/health/HealthEndpoint.java | 3 + .../health/OverallHealthIndicator.java | 63 +++++++++ .../OverallReactiveHealthIndicator.java | 53 +++++++ .../health/ReactiveAggregatedHealth.java | 71 ++++++++++ .../ReactiveHealthEndpointWebExtension.java | 3 + .../actuate/health/AggregatedHealthTests.java | 91 ++++++++++++ .../health/GroupHealthIndicatorTests.java | 71 ++++++++++ .../GroupReactiveHealthIndicatorTests.java | 73 ++++++++++ .../health/OverallHealthIndicatorTests.java | 124 ++++++++++++++++ .../OverallReactiveHealthIndicatorTests.java | 133 ++++++++++++++++++ .../health/ReactiveAggregatedHealthTests.java | 98 +++++++++++++ .../src/main/resources/application.properties | 4 + 21 files changed, 1160 insertions(+), 30 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealth.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedReactiveHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealth.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AggregatedHealthTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealthTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java index 8e2cfde2a438..853a30ae0a14 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java @@ -17,10 +17,10 @@ package org.springframework.boot.actuate.autoconfigure.health; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.actuate.health.CompositeHealthIndicator; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthIndicatorRegistry; +import org.springframework.boot.actuate.health.OverallHealthIndicator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.context.annotation.Bean; @@ -39,7 +39,7 @@ class HealthEndpointConfiguration { @Bean @ConditionalOnMissingBean HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry) { - return new HealthEndpoint(new CompositeHealthIndicator(healthAggregator, registry)); + return new HealthEndpoint(new OverallHealthIndicator(healthAggregator, registry)); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index af893e2ef895..2f3569b044c0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -16,15 +16,17 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.util.Map; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.actuate.health.HealthStatusHttpMapper; import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper; import org.springframework.boot.actuate.health.OrderedHealthAggregator; +import org.springframework.boot.actuate.health.OverallReactiveHealthIndicator; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -48,10 +50,11 @@ class HealthEndpointWebExtensionConfiguration { @Bean @ConditionalOnMissingBean - HealthStatusHttpMapper createHealthStatusHttpMapper(HealthIndicatorProperties healthIndicatorProperties) { + HealthStatusHttpMapper createHealthStatusHttpMapper(HealthIndicatorProperties properties) { HealthStatusHttpMapper statusHttpMapper = new HealthStatusHttpMapper(); - if (healthIndicatorProperties.getHttpMapping() != null) { - statusHttpMapper.addStatusMapping(healthIndicatorProperties.getHttpMapping()); + Map httpMapping = properties.getStatus().getHttpMapping(); + if (httpMapping != null) { + statusHttpMapper.addStatusMapping(httpMapping); } return statusHttpMapper; } @@ -75,7 +78,7 @@ static class ReactiveWebHealthConfiguration { ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension( ObjectProvider healthAggregator, ReactiveHealthIndicatorRegistry registry, HealthWebEndpointResponseMapper responseMapper) { - return new ReactiveHealthEndpointWebExtension(new CompositeReactiveHealthIndicator( + return new ReactiveHealthEndpointWebExtension(new OverallReactiveHealthIndicator( healthAggregator.getIfAvailable(OrderedHealthAggregator::new), registry), responseMapper); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java index b680c9d22092..49b7e3435dd5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java @@ -16,11 +16,17 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import java.util.function.Function; import reactor.core.publisher.Flux; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; +import org.springframework.boot.actuate.health.GroupHealthIndicator; +import org.springframework.boot.actuate.health.GroupReactiveHealthIndicator; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicatorRegistry; @@ -59,16 +65,40 @@ public ApplicationHealthIndicator applicationHealthIndicator() { @ConditionalOnMissingBean(HealthAggregator.class) public OrderedHealthAggregator healthAggregator(HealthIndicatorProperties properties) { OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator(); - if (properties.getOrder() != null) { - healthAggregator.setStatusOrder(properties.getOrder()); + if (properties.getStatus().getOrder() != null) { + healthAggregator.setStatusOrder(properties.getStatus().getOrder()); } return healthAggregator; } @Bean @ConditionalOnMissingBean(HealthIndicatorRegistry.class) - public HealthIndicatorRegistry healthIndicatorRegistry(ApplicationContext applicationContext) { - return HealthIndicatorRegistryBeans.get(applicationContext); + public HealthIndicatorRegistry healthIndicatorRegistry(HealthIndicatorProperties properties, + HealthAggregator healthAggregator, ApplicationContext applicationContext) { + HealthIndicatorRegistry registry = HealthIndicatorRegistryBeans.get(applicationContext); + extractGroups(properties, registry::get).forEach((groupName, groupHealthIndicators) -> registry + .register(groupName, new GroupHealthIndicator(healthAggregator, registry, groupHealthIndicators))); + return registry; + } + + private static Map> extractGroups(HealthIndicatorProperties properties, + Function healthIndicatorByName) { + Map> groupDefinitions = new LinkedHashMap<>(); + properties.getGroups().forEach((groupName, indicatorNames) -> { + if (healthIndicatorByName.apply(groupName) != null) { + throw new IllegalArgumentException("Could not register health indicator group named '" + groupName + + "', an health indicator with that name is already registered"); + } + Set groupHealthIndicators = new LinkedHashSet<>(); + indicatorNames.forEach((name) -> { + T healthIndicator = healthIndicatorByName.apply(name); + if (healthIndicator != null) { + groupHealthIndicators.add(name); + } + }); + groupDefinitions.put(groupName, groupHealthIndicators); + }); + return groupDefinitions; } @Configuration(proxyBeanMethods = false) @@ -77,11 +107,15 @@ static class ReactiveHealthIndicatorConfiguration { @Bean @ConditionalOnMissingBean - ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry( - Map reactiveHealthIndicators, + ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry(HealthIndicatorProperties properties, + HealthAggregator healthAggregator, Map reactiveHealthIndicators, Map healthIndicators) { - return new ReactiveHealthIndicatorRegistryFactory() + ReactiveHealthIndicatorRegistry registry = new ReactiveHealthIndicatorRegistryFactory() .createReactiveHealthIndicatorRegistry(reactiveHealthIndicators, healthIndicators); + extractGroups(properties, registry::get) + .forEach((groupName, groupHealthIndicators) -> registry.register(groupName, + new GroupReactiveHealthIndicator(healthAggregator, registry, groupHealthIndicators))); + return registry; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java index 79bccb47d46b..a193c96d82ed 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java @@ -28,32 +28,52 @@ * @author Christian Dupuis * @since 2.0.0 */ -@ConfigurationProperties(prefix = "management.health.status") +@ConfigurationProperties(prefix = "management.health") public class HealthIndicatorProperties { /** - * Comma-separated list of health statuses in order of severity. + * Health indicator groups. Each entry maps the name of a group with a list of health + * indicators to associate with the group. */ - private List order = null; + private final Map> groups = new HashMap<>(); - /** - * Mapping of health statuses to HTTP status codes. By default, registered health - * statuses map to sensible defaults (for example, UP maps to 200). - */ - private final Map httpMapping = new HashMap<>(); + private final Status status = new Status(); - public List getOrder() { - return this.order; + public Map> getGroups() { + return this.groups; } - public void setOrder(List statusOrder) { - if (statusOrder != null && !statusOrder.isEmpty()) { - this.order = statusOrder; - } + public Status getStatus() { + return this.status; } - public Map getHttpMapping() { - return this.httpMapping; + public static class Status { + + /** + * Comma-separated list of health statuses in order of severity. + */ + private List order = null; + + /** + * Mapping of health statuses to HTTP status codes. By default, registered health + * statuses map to sensible defaults (for example, UP maps to 200). + */ + private final Map httpMapping = new HashMap<>(); + + public List getOrder() { + return this.order; + } + + public void setOrder(List statusOrder) { + if (statusOrder != null && !statusOrder.isEmpty()) { + this.order = statusOrder; + } + } + + public Map getHttpMapping() { + return this.httpMapping; + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealth.java new file mode 100644 index 000000000000..5c615fedbf77 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealth.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.HashMap; +import java.util.Map; + +/** + * Aggregates the overall health of a system by caching {@link Health} per indicator. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +public class AggregatedHealth { + + private static final Health UNKNOWN_HEALTH = Health.unknown().build(); + + private final HealthIndicatorRegistry registry; + + private final Map healths; + + /** + * Create an instance based on the specified {@link HealthIndicatorRegistry}. + * @param registry the registry to use to retrieve an indicator by name + */ + public AggregatedHealth(HealthIndicatorRegistry registry) { + this.registry = registry; + this.healths = new HashMap<>(); + } + + /** + * Return the {@link Health} of the indicator with the specified {@code name} for this + * instance or {@code null} if no such indicator exists. When calling this method + * several times for a given indicator, the same {@link Health} instance is returned. + * @param name the name of a {@link HealthIndicator} + * @return the {@link Health} of the indicator with the specified name + */ + public Health health(String name) { + Health health = this.healths.computeIfAbsent(name, this::determineHealth); + return (health != UNKNOWN_HEALTH) ? health : null; + } + + private Health determineHealth(String name) { + HealthIndicator healthIndicator = this.registry.get(name); + if (healthIndicator == null) { + return UNKNOWN_HEALTH; + } + if (healthIndicator instanceof AggregatedHealthIndicator) { + return ((AggregatedHealthIndicator) healthIndicator).health(this); + } + return healthIndicator.health(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealthIndicator.java new file mode 100644 index 000000000000..497eb58efc1d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedHealthIndicator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2019 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.health; + +/** + * An extended {@link HealthIndicator} that computes a global {@link Health} based on a + * {@link AggregatedHealth}. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +@FunctionalInterface +public interface AggregatedHealthIndicator extends HealthIndicator { + + @Override + default Health health() { + return health(new AggregatedHealth(new DefaultHealthIndicatorRegistry())); + } + + /** + * Return an indication of health based on the specified {@link AggregatedHealth}. + * @param aggregatedHealth the already computed health + * @return the health that this aggregate represents + */ + Health health(AggregatedHealth aggregatedHealth); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedReactiveHealthIndicator.java new file mode 100644 index 000000000000..6f7fb817f1ca --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AggregatedReactiveHealthIndicator.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 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.health; + +import reactor.core.publisher.Mono; + +/** + * An extended {@link ReactiveHealthIndicator} that computes a global {@link Health} based + * on a {@link ReactiveAggregatedHealth}. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +@FunctionalInterface +public interface AggregatedReactiveHealthIndicator extends ReactiveHealthIndicator { + + @Override + default Mono health() { + return health(new ReactiveAggregatedHealth(new DefaultReactiveHealthIndicatorRegistry())); + } + + /** + * Return an indication of health based on the specified + * {@link ReactiveAggregatedHealth}. + * @param aggregatedHealth the already computed health + * @return a {@link Mono} to the health that this aggregate represents + */ + Mono health(ReactiveAggregatedHealth aggregatedHealth); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupHealthIndicator.java new file mode 100644 index 000000000000..00d4541f4580 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupHealthIndicator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * An {@link AggregatedHealthIndicator} that groups existing indicators together. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +public class GroupHealthIndicator implements AggregatedHealthIndicator { + + private final HealthAggregator aggregator; + + private final HealthIndicatorRegistry registry; + + private final Set indicatorNames; + + /** + * Create a group using the specified {@link HealthAggregator} and indicators. + * @param aggregator the health aggregator to use + * @param registry the registry + * @param indicatorNames the names of the health indicators to include in the group + */ + public GroupHealthIndicator(HealthAggregator aggregator, HealthIndicatorRegistry registry, + Set indicatorNames) { + this.aggregator = aggregator; + this.registry = registry; + this.indicatorNames = new LinkedHashSet<>(indicatorNames); + } + + @Override + public Health health() { + return health(new AggregatedHealth(this.registry)); + } + + @Override + public Health health(AggregatedHealth aggregatedHealth) { + Map healths = new LinkedHashMap<>(); + for (String indicatorName : this.indicatorNames) { + Health health = aggregatedHealth.health(indicatorName); + if (health != null) { + healths.put(indicatorName, health); + } + } + return this.aggregator.aggregate(healths); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicator.java new file mode 100644 index 000000000000..c1662cc4a632 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Set; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +/** + * A {@link AggregatedReactiveHealthIndicator} that groups existing indicators together. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +public class GroupReactiveHealthIndicator implements AggregatedReactiveHealthIndicator { + + private final HealthAggregator aggregator; + + private final ReactiveHealthIndicatorRegistry registry; + + private final Set indicatorNames; + + /** + * Create a group using the specified {@link HealthAggregator} and indicators. + * @param aggregator the health aggregator to use + * @param registry the registry + * @param indicatorNames the names of the health indicators to include in the group + */ + public GroupReactiveHealthIndicator(HealthAggregator aggregator, ReactiveHealthIndicatorRegistry registry, + Set indicatorNames) { + this.aggregator = aggregator; + this.registry = registry; + this.indicatorNames = indicatorNames; + } + + @Override + public Mono health() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + return health(aggregatedHealth); + } + + @Override + public Mono health(ReactiveAggregatedHealth aggregatedHealth) { + return Flux.fromIterable(this.indicatorNames) + .flatMap((name) -> Mono.zip(Mono.just(name), aggregatedHealth.health(name))) + .collectMap(Tuple2::getT1, Tuple2::getT2).map(this.aggregator::aggregate); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java index 541b52cbc1ba..9b4ecdeb3d34 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java @@ -81,6 +81,9 @@ private HealthIndicator getNestedHealthIndicator(HealthIndicator healthIndicator if (healthIndicator instanceof CompositeHealthIndicator) { return ((CompositeHealthIndicator) healthIndicator).getRegistry().get(name); } + if (healthIndicator instanceof OverallHealthIndicator) { + return ((OverallHealthIndicator) healthIndicator).getRegistry().get(name); + } return null; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallHealthIndicator.java new file mode 100644 index 000000000000..6393aa1ece8e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallHealthIndicator.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An overall {@link HealthIndicator} that creates a {@link Health} based on all the known + * indicators of a {@link HealthIndicatorRegistry}. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +public final class OverallHealthIndicator implements HealthIndicator { + + private final HealthAggregator aggregator; + + private final HealthIndicatorRegistry registry; + + /** + * Create a new instance with the specified {@link HealthAggregator} and + * {@link HealthIndicatorRegistry}. + * @param aggregator the health aggregator to compute the overall status + * @param registry the registry to use to identify the indicators to invoke + */ + public OverallHealthIndicator(HealthAggregator aggregator, HealthIndicatorRegistry registry) { + this.aggregator = aggregator; + this.registry = registry; + } + + HealthIndicatorRegistry getRegistry() { + return this.registry; + } + + @Override + public Health health() { + AggregatedHealth aggregatedHealth = new AggregatedHealth(this.registry); + Map allIndicators = new LinkedHashMap<>(); + this.registry.getAll().keySet().forEach((name) -> { + Health health = aggregatedHealth.health(name); + if (health != null) { + allIndicators.put(name, health); + } + }); + return this.aggregator.aggregate(allIndicators); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicator.java new file mode 100644 index 000000000000..babf6619c83f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2019 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.health; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +/** + * A overall {@link ReactiveHealthIndicator} that creates a {@link Health} based on all + * the known indicators of a {@link ReactiveHealthIndicatorRegistry}. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +public class OverallReactiveHealthIndicator implements ReactiveHealthIndicator { + + private final HealthAggregator healthAggregator; + + private final ReactiveHealthIndicatorRegistry registry; + + public OverallReactiveHealthIndicator(HealthAggregator healthAggregator, ReactiveHealthIndicatorRegistry registry) { + this.healthAggregator = healthAggregator; + this.registry = registry; + } + + public ReactiveHealthIndicatorRegistry getRegistry() { + return this.registry; + } + + @Override + public Mono health() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + return Flux.fromIterable(this.registry.getAll().entrySet()) + .flatMap((entry) -> Mono.zip(Mono.just(entry.getKey()), aggregatedHealth.health(entry.getKey()))) + .collectMap(Tuple2::getT1, Tuple2::getT2).map(this.healthAggregator::aggregate); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealth.java new file mode 100644 index 000000000000..4f2f8a85a8ba --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealth.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.HashMap; +import java.util.Map; + +import reactor.core.publisher.Mono; + +/** + * Aggregates the overall health of a system by caching {@link Health} per indicator. + * + * @author Stephane Nicoll + * @since 2.2.0 + */ +public class ReactiveAggregatedHealth { + + private static final Mono UNKNOWN_HEALTH = Mono.empty(); + + private final ReactiveHealthIndicatorRegistry registry; + + private final Map> healths; + + /** + * Create an instance based on the specified {@link ReactiveHealthIndicatorRegistry}. + * @param registry the registry to use to retrieve an indicator by name + */ + public ReactiveAggregatedHealth(ReactiveHealthIndicatorRegistry registry) { + this.registry = registry; + this.healths = new HashMap<>(); + } + + /** + * Return the {@link Health} of the indicator with the specified {@code name} for this + * instance or {@link Mono#empty()} if no such indicator exists. When calling this + * method several times for a given indicator, the same {@link Health} instance is + * returned. + * @param name the name of a {@link HealthIndicator} + * @return a cached {@link Mono} that provides the {@link Health} of the indicator + * with the specified name + */ + public Mono health(String name) { + return this.healths.computeIfAbsent(name, (indicator) -> determineHealth(name).cache()); + } + + private Mono determineHealth(String name) { + ReactiveHealthIndicator healthIndicator = this.registry.get(name); + if (healthIndicator == null) { + return UNKNOWN_HEALTH; + } + if (healthIndicator instanceof AggregatedReactiveHealthIndicator) { + return ((AggregatedReactiveHealthIndicator) healthIndicator).health(this); + } + return healthIndicator.health(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java index 689ea9d46de7..a4fa11789e5d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java @@ -79,6 +79,9 @@ private ReactiveHealthIndicator getNestedHealthIndicator(ReactiveHealthIndicator if (healthIndicator instanceof CompositeReactiveHealthIndicator) { return ((CompositeReactiveHealthIndicator) healthIndicator).getRegistry().get(name); } + if (healthIndicator instanceof OverallReactiveHealthIndicator) { + return ((OverallReactiveHealthIndicator) healthIndicator).getRegistry().get(name); + } return null; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AggregatedHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AggregatedHealthTests.java new file mode 100644 index 000000000000..c50993e31314 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AggregatedHealthTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AggregatedHealth}. + * + * @author Stephane Nicoll + */ +class AggregatedHealthTests { + + @Mock + private HealthIndicator one; + + private HealthIndicatorRegistry registry; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + given(this.one.health()).willReturn(new Health.Builder().up().withDetail("1", "1").build()); + this.registry = new DefaultHealthIndicatorRegistry(Collections.singletonMap("one", this.one)); + } + + @Test + void healthForKnownIndicator() { + AggregatedHealth aggregatedHealth = new AggregatedHealth(this.registry); + assertThat(aggregatedHealth.health("one")).isEqualTo(new Health.Builder().up().withDetail("1", "1").build()); + } + + @Test + void healthForKnownIndicatorInvokesTargetHealthIndicatorOnce() { + AggregatedHealth aggregatedHealth = new AggregatedHealth(this.registry); + Health first = aggregatedHealth.health("one"); + Health second = aggregatedHealth.health("one"); + assertThat(first).isSameAs(second); + verify(this.one, times(1)).health(); + } + + @Test + void healthForKnownAggregatedHealthIndicatorUsesAggregatedHealth() { + AggregatedHealth aggregatedHealth = new AggregatedHealth(this.registry); + AggregatedHealthIndicator two = mock(AggregatedHealthIndicator.class); + given(two.health(aggregatedHealth)).willReturn(new Health.Builder().down().withDetail("2", "2").build()); + this.registry.register("two", two); + assertThat(aggregatedHealth.health("two")).isEqualTo(new Health.Builder().down().withDetail("2", "2").build()); + verify(two, times(1)).health(aggregatedHealth); + } + + @Test + void healthForUnknownIndicator() { + AggregatedHealth aggregatedHealth = new AggregatedHealth(this.registry); + assertThat(aggregatedHealth.health("unknown")).isNull(); + } + + @Test + void healthForUnknownIndicatorIsCached() { + AggregatedHealth aggregatedHealth = new AggregatedHealth(this.registry); + assertThat(aggregatedHealth.health("unknown")).isNull(); + // Register indicator after it has been requested for this round + this.registry.register("unknown", () -> Health.unknown().build()); + assertThat(aggregatedHealth.health("unknown")).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupHealthIndicatorTests.java new file mode 100644 index 000000000000..5cf8dc7799b3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupHealthIndicatorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link GroupHealthIndicator}. + * + * @author Stephane Nicoll + */ +class GroupHealthIndicatorTests { + + private final HealthAggregator aggregator = new OrderedHealthAggregator(); + + private final HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry(); + + @Test + void groupHealthInvokesIndicatorsByName() { + AggregatedHealth aggregatedHealth = mock(AggregatedHealth.class); + given(aggregatedHealth.health("one")).willReturn(Health.up().build()); + given(aggregatedHealth.health("two")).willReturn(Health.up().build()); + Health health = createGroupHealthIndicator("one", "two").health(aggregatedHealth); + assertThat(health.getDetails()).containsOnlyKeys("one", "two"); + verify(aggregatedHealth).health("one"); + verify(aggregatedHealth).health("two"); + verifyZeroInteractions(aggregatedHealth); + } + + @Test + void groupHealthSkipDisabledIndicator() { + AggregatedHealth aggregatedHealth = mock(AggregatedHealth.class); + given(aggregatedHealth.health("one")).willReturn(Health.up().build()); + given(aggregatedHealth.health("two")).willReturn(null); + given(aggregatedHealth.health("three")).willReturn(Health.up().build()); + Health health = createGroupHealthIndicator("one", "two").health(aggregatedHealth); + assertThat(health.getDetails()).containsOnlyKeys("one"); + verify(aggregatedHealth).health("one"); + verify(aggregatedHealth).health("two"); + verifyZeroInteractions(aggregatedHealth); + } + + private GroupHealthIndicator createGroupHealthIndicator(String... indicatorNames) { + return new GroupHealthIndicator(this.aggregator, this.registry, + new LinkedHashSet<>(Arrays.asList(indicatorNames))); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicatorTests.java new file mode 100644 index 000000000000..be6b27e178e4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/GroupReactiveHealthIndicatorTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link GroupReactiveHealthIndicator}. + * + * @author Stephane Nicoll + */ +class GroupReactiveHealthIndicatorTests { + + private final HealthAggregator aggregator = new OrderedHealthAggregator(); + + private final ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry(); + + @Test + void groupHealthInvokesIndicatorsByName() { + ReactiveAggregatedHealth aggregatedHealth = mock(ReactiveAggregatedHealth.class); + given(aggregatedHealth.health("one")).willReturn(Mono.just(Health.up().build())); + given(aggregatedHealth.health("two")).willReturn(Mono.just(Health.up().build())); + StepVerifier.create(createGroupReactiveHealthIndicator("one", "two").health(aggregatedHealth)) + .consumeNextWith((h) -> assertThat(h.getDetails()).containsOnlyKeys("one", "two")).verifyComplete(); + verify(aggregatedHealth).health("one"); + verify(aggregatedHealth).health("two"); + verifyZeroInteractions(aggregatedHealth); + } + + @Test + void groupHealthSkipDisabledIndicator() { + ReactiveAggregatedHealth aggregatedHealth = mock(ReactiveAggregatedHealth.class); + given(aggregatedHealth.health("one")).willReturn(Mono.just(Health.up().build())); + given(aggregatedHealth.health("two")).willReturn(Mono.empty()); + given(aggregatedHealth.health("three")).willReturn(Mono.just(Health.up().build())); + StepVerifier.create(createGroupReactiveHealthIndicator("one", "two").health(aggregatedHealth)) + .consumeNextWith((h) -> assertThat(h.getDetails()).containsOnlyKeys("one")).verifyComplete(); + verify(aggregatedHealth).health("one"); + verify(aggregatedHealth).health("two"); + verifyZeroInteractions(aggregatedHealth); + } + + private GroupReactiveHealthIndicator createGroupReactiveHealthIndicator(String... indicatorNames) { + return new GroupReactiveHealthIndicator(this.aggregator, this.registry, + new LinkedHashSet<>(Arrays.asList(indicatorNames))); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallHealthIndicatorTests.java new file mode 100644 index 000000000000..9bcdb48fbde2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallHealthIndicatorTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link OverallHealthIndicator}. + * + * @author Stephane Nicoll + */ +class OverallHealthIndicatorTests { + + @Mock + private HealthIndicator one; + + @Mock + private HealthIndicator two; + + private final HealthAggregator healthAggregator = new OrderedHealthAggregator(); + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + given(this.one.health()).willReturn(Health.up().withDetail("1", "1").build()); + given(this.two.health()).willReturn(Health.up().withDetail("2", "2").build()); + } + + @Test + void overallHealthUsesAllIndicatorsFromRegistry() { + HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry( + Collections.singletonMap("one", this.one)); + registry.register("two", this.two); + Health health = new OverallHealthIndicator(this.healthAggregator, registry).health(); + assertThat(health.getDetails()).containsOnlyKeys("one", "two"); + } + + @Test + void overallHealthIsUsedByNestedAggregatedHealthIndicator() { + AggregatedHealthIndicator aggregate = mock(AggregatedHealthIndicator.class); + given(aggregate.health()).willThrow(new IllegalStateException("Should not be called")); + given(aggregate.health(any(AggregatedHealth.class))).willReturn(Health.up().build()); + HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry( + Collections.singletonMap("test", aggregate)); + Health health = new OverallHealthIndicator(this.healthAggregator, registry).health(); + verify(aggregate).health(any(AggregatedHealth.class)); + assertThat(health.getDetails()).containsOnly(entry("test", Health.up().build())); + } + + @Test + void overallHealthSkipUnregisteredIndicators() { + HealthIndicatorRegistry registry = mock(HealthIndicatorRegistry.class); + Map allIndicators = new HashMap<>(); + allIndicators.put("one", this.one); + allIndicators.put("two", this.two); + given(registry.getAll()).willReturn(allIndicators); + given(registry.get("one")).willReturn(this.one); + Health health = new OverallHealthIndicator(this.healthAggregator, registry).health(); + assertThat(health.getDetails()).containsOnly(entry("one", Health.up().withDetail("1", "1").build())); + verify(this.one).health(); + verifyZeroInteractions(this.two); + } + + @Test + void overallHealthUseIdenticalHealthForKnownIndicator() { + HealthIndicator random = mock(HealthIndicator.class); + given(random.health()).willReturn(Health.up().withDetail("uuid", UUID.randomUUID().toString()).build()); + Map allIndicators = new HashMap<>(); + allIndicators.put("one", this.one); + allIndicators.put("two", this.two); + allIndicators.put("random", random); + HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry(allIndicators); + registry.register("group1", new GroupHealthIndicator(this.healthAggregator, registry, + new LinkedHashSet<>(Arrays.asList("one", "random")))); + registry.register("group2", new GroupHealthIndicator(this.healthAggregator, registry, + new LinkedHashSet<>(Arrays.asList("two", "random")))); + Health health = new OverallHealthIndicator(this.healthAggregator, registry).health(); + verify(this.one).health(); + verify(this.two).health(); + verify(random).health(); + Object uuid = ((Health) health.getDetails().get("random")).getDetails().get("uuid"); + assertThat(health.getDetails()).containsOnly(entry("one", Health.up().withDetail("1", "1").build()), + entry("two", Health.up().withDetail("2", "2").build()), + entry("random", Health.up().withDetail("uuid", uuid).build()), + entry("group1", + Health.up().withDetail("one", Health.up().withDetail("1", "1").build()) + .withDetail("random", Health.up().withDetail("uuid", uuid).build()).build()), + entry("group2", Health.up().withDetail("two", Health.up().withDetail("2", "2").build()) + .withDetail("random", Health.up().withDetail("uuid", uuid).build()).build())); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicatorTests.java new file mode 100644 index 000000000000..2577546ec26e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OverallReactiveHealthIndicatorTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link OverallReactiveHealthIndicator}. + * + * @author Stephane Nicoll + */ +class OverallReactiveHealthIndicatorTests { + + @Mock + private ReactiveHealthIndicator one; + + @Mock + private ReactiveHealthIndicator two; + + private final HealthAggregator healthAggregator = new OrderedHealthAggregator(); + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + given(this.one.health()).willReturn(Mono.just(Health.up().withDetail("1", "1").build())); + given(this.two.health()).willReturn(Mono.just(Health.up().withDetail("2", "2").build())); + } + + @Test + void overallHealthUsesAllIndicatorsFromRegistry() { + ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry( + Collections.singletonMap("one", this.one)); + registry.register("two", this.two); + StepVerifier.create(new OverallReactiveHealthIndicator(this.healthAggregator, registry).health()) + .consumeNextWith((health) -> assertThat(health.getDetails()).containsOnlyKeys("one", "two")) + .verifyComplete(); + } + + @Test + void overallHealthIsUsedByNestedAggregatedHealthIndicator() { + AggregatedReactiveHealthIndicator aggregate = mock(AggregatedReactiveHealthIndicator.class); + given(aggregate.health()).willThrow(new IllegalStateException("Should not be called")); + given(aggregate.health(any(ReactiveAggregatedHealth.class))).willReturn(Mono.just(Health.up().build())); + ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry( + Collections.singletonMap("test", aggregate)); + StepVerifier.create(new OverallReactiveHealthIndicator(this.healthAggregator, registry).health()) + .consumeNextWith( + (health) -> assertThat(health.getDetails()).containsOnly(entry("test", Health.up().build()))) + .verifyComplete(); + verify(aggregate).health(any(ReactiveAggregatedHealth.class)); + } + + @Test + void overallHealthSkipUnregisteredIndicators() { + ReactiveHealthIndicatorRegistry registry = mock(ReactiveHealthIndicatorRegistry.class); + Map allIndicators = new HashMap<>(); + allIndicators.put("one", this.one); + allIndicators.put("two", this.two); + given(registry.getAll()).willReturn(allIndicators); + given(registry.get("one")).willReturn(this.one); + StepVerifier.create(new OverallReactiveHealthIndicator(this.healthAggregator, registry).health()) + .consumeNextWith((health) -> assertThat(health.getDetails()) + .containsOnly(entry("one", Health.up().withDetail("1", "1").build()))) + .verifyComplete(); + verify(this.one).health(); + verifyZeroInteractions(this.two); + } + + @Test + void overallHealthUseIdenticalHealthForKnownIndicator() { + ReactiveHealthIndicator random = mock(ReactiveHealthIndicator.class); + given(random.health()) + .willReturn(Mono.just(Health.up().withDetail("uuid", UUID.randomUUID().toString()).build())); + Map allIndicators = new HashMap<>(); + allIndicators.put("one", this.one); + allIndicators.put("two", this.two); + allIndicators.put("random", random); + ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry(allIndicators); + registry.register("group1", new GroupReactiveHealthIndicator(this.healthAggregator, registry, + new LinkedHashSet<>(Arrays.asList("one", "random")))); + registry.register("group2", new GroupReactiveHealthIndicator(this.healthAggregator, registry, + new LinkedHashSet<>(Arrays.asList("two", "random")))); + StepVerifier.create(new OverallReactiveHealthIndicator(this.healthAggregator, registry).health()) + .consumeNextWith((health) -> { + Object uuid = ((Health) health.getDetails().get("random")).getDetails().get("uuid"); + assertThat(health.getDetails()).containsOnly(entry("one", Health.up().withDetail("1", "1").build()), + entry("two", Health.up().withDetail("2", "2").build()), + entry("random", Health.up().withDetail("uuid", uuid).build()), + entry("group1", Health.up().withDetail("one", Health.up().withDetail("1", "1").build()) + .withDetail("random", Health.up().withDetail("uuid", uuid).build()).build()), + entry("group2", Health.up().withDetail("two", Health.up().withDetail("2", "2").build()) + .withDetail("random", Health.up().withDetail("uuid", uuid).build()).build())); + }).verifyComplete(); + verify(random).health(); + verify(this.one).health(); + verify(this.two).health(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealthTests.java new file mode 100644 index 000000000000..8de3a2c38409 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveAggregatedHealthTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2019 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.health; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ReactiveAggregatedHealth}. + * + * @author Stephane Nicoll + */ +class ReactiveAggregatedHealthTests { + + @Mock + private ReactiveHealthIndicator one; + + private ReactiveHealthIndicatorRegistry registry; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + given(this.one.health()).willReturn(Mono.just(new Health.Builder().up().withDetail("1", "1").build())); + this.registry = new DefaultReactiveHealthIndicatorRegistry(Collections.singletonMap("one", this.one)); + } + + @Test + void healthForKnownIndicator() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + StepVerifier.create(aggregatedHealth.health("one")).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).containsOnly(entry("1", "1")); + }).verifyComplete(); + } + + @Test + void healthForKnownIndicatorInvokesTargetHealthIndicatorOnce() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + Health first = aggregatedHealth.health("one").block(); + Health second = aggregatedHealth.health("one").block(); + assertThat(first).isSameAs(second); + verify(this.one, times(1)).health(); + } + + @Test + void healthForKnownAggregatedHealthIndicatorUsesAggregatedHealth() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + AggregatedReactiveHealthIndicator two = mock(AggregatedReactiveHealthIndicator.class); + given(two.health(aggregatedHealth)).willReturn(Mono.just(Health.up().withDetail("2", "2").build())); + this.registry.register("two", two); + assertThat(aggregatedHealth.health("two").block()) + .isEqualTo(new Health.Builder().up().withDetail("2", "2").build()); + verify(two, times(1)).health(aggregatedHealth); + } + + @Test + void healthForUnknownIndicator() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + StepVerifier.create(aggregatedHealth.health("unknown")).verifyComplete(); + } + + @Test + void healthForUnknownIndicatorIsCached() { + ReactiveAggregatedHealth aggregatedHealth = new ReactiveAggregatedHealth(this.registry); + StepVerifier.create(aggregatedHealth.health("unknown")).verifyComplete(); + // Register indicator after it has been requested for this round + this.registry.register("unknown", () -> Mono.just(Health.unknown().build())); + StepVerifier.create(aggregatedHealth.health("unknown")).verifyComplete(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 09413b04851a..32b68763cec6 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -19,3 +19,7 @@ spring.jmx.enabled=true spring.jackson.serialization.write_dates_as_timestamps=false management.trace.http.include=request-headers,response-headers,principal,remote-address,session-id + +management.health.groups.ready=db,diskSpace +management.health.groups.live=example,hello,db +management.endpoint.health.show-details=always