diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java index 227b2f207a..5acd02925e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java @@ -20,8 +20,10 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator; +import org.springframework.boot.actuate.health.DefaultReactiveHealthIndicatorRegistry; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; import org.springframework.core.ResolvableType; /** @@ -41,11 +43,10 @@ public abstract class CompositeReactiveHealthIndicatorConfiguration composite.addHealthIndicator(name, + ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry(); + beans.forEach((name, source) -> registry.register(name, createHealthIndicator(source))); - return composite; + return new CompositeReactiveHealthIndicator(this.healthAggregator, registry); } @SuppressWarnings("unchecked") 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 3da41f852e..e140ac2e74 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,23 +16,21 @@ package org.springframework.boot.actuate.autoconfigure.health; -import java.util.Collections; -import java.util.Map; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; -import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicatorFactory; +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.HealthIndicator; 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.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -70,19 +68,16 @@ class HealthEndpointWebExtensionConfiguration { @Configuration @ConditionalOnWebApplication(type = Type.REACTIVE) + @ConditionalOnSingleCandidate(ReactiveHealthIndicatorRegistry.class) static class ReactiveWebHealthConfiguration { private final ReactiveHealthIndicator reactiveHealthIndicator; ReactiveWebHealthConfiguration(ObjectProvider healthAggregator, - ObjectProvider> reactiveHealthIndicators, - ObjectProvider> healthIndicators) { - this.reactiveHealthIndicator = new CompositeReactiveHealthIndicatorFactory() - .createReactiveHealthIndicator( - healthAggregator.getIfAvailable(OrderedHealthAggregator::new), - reactiveHealthIndicators - .getIfAvailable(Collections::emptyMap), - healthIndicators.getIfAvailable(Collections::emptyMap)); + ReactiveHealthIndicatorRegistry registry) { + this.reactiveHealthIndicator = new CompositeReactiveHealthIndicator( + healthAggregator.getIfAvailable(OrderedHealthAggregator::new), + registry); } @Bean 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 5be8f3ab13..43ac74cbf9 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,13 +16,22 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.util.Collections; +import java.util.Map; + +import reactor.core.publisher.Flux; + +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; +import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistryFactory; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; @@ -71,4 +80,20 @@ public class HealthIndicatorAutoConfiguration { return HealthIndicatorRegistryBeans.get(applicationContext); } + @Configuration + @ConditionalOnClass(Flux.class) + static class ReactiveHealthIndicatorConfiguration { + + @Bean + @ConditionalOnMissingBean + public ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry( + ObjectProvider> reactiveHealthIndicators, + ObjectProvider> healthIndicators) { + return new ReactiveHealthIndicatorRegistryFactory().createReactiveHealthIndicatorRegistry( + reactiveHealthIndicators.getIfAvailable(Collections::emptyMap), + healthIndicators.getIfAvailable(Collections::emptyMap)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorBeansReactiveComposite.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorBeansReactiveComposite.java deleted file mode 100644 index 2b08d4872a..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorBeansReactiveComposite.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2017 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 - * - * http://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.health; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator; -import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicatorFactory; -import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.OrderedHealthAggregator; -import org.springframework.boot.actuate.health.ReactiveHealthIndicator; -import org.springframework.context.ApplicationContext; - -/** - * Creates a {@link CompositeReactiveHealthIndicator} from beans in the - * {@link ApplicationContext}. - * - * @author Phillip Webb - */ -final class HealthIndicatorBeansReactiveComposite { - - private HealthIndicatorBeansReactiveComposite() { - } - - public static ReactiveHealthIndicator get(ApplicationContext applicationContext) { - HealthAggregator healthAggregator = getHealthAggregator(applicationContext); - return new CompositeReactiveHealthIndicatorFactory() - .createReactiveHealthIndicator(healthAggregator, - applicationContext.getBeansOfType(ReactiveHealthIndicator.class), - applicationContext.getBeansOfType(HealthIndicator.class)); - } - - private static HealthAggregator getHealthAggregator( - ApplicationContext applicationContext) { - try { - return applicationContext.getBean(HealthAggregator.class); - } - catch (NoSuchBeanDefinitionException ex) { - return new OrderedHealthAggregator(); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointWebExtensionTests.java index 2282b1a9dc..873a4a6118 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointWebExtensionTests.java @@ -28,6 +28,7 @@ import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -217,6 +218,24 @@ public class ReactiveHealthEndpointWebExtensionTests { }); } + @Test + public void registryCanBeAltered() { + this.contextRunner + .withUserConfiguration(HealthIndicatorsConfiguration.class) + .withPropertyValues("management.endpoint.health.show-details=always") + .run((context) -> { + ReactiveHealthIndicatorRegistry registry = context.getBean( + ReactiveHealthIndicatorRegistry.class); + ReactiveHealthEndpointWebExtension extension = context + .getBean(ReactiveHealthEndpointWebExtension.class); + assertThat(extension.health(null).block().getBody().getDetails()) + .containsOnlyKeys("application", "first", "second"); + assertThat(registry.unregister("second")).isNotNull(); + assertThat(extension.health(null).block().getBody().getDetails()) + .containsKeys("application", "first"); + }); + } + @Configuration static class HealthIndicatorsConfiguration { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java index 36879f0692..a6d291fa6d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java @@ -25,8 +25,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; -import org.springframework.util.Assert; - /** * {@link ReactiveHealthIndicator} that returns health indications from all registered * delegates. Provides an alternative {@link Health} for a delegate that reaches a @@ -37,7 +35,7 @@ import org.springframework.util.Assert; */ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator { - private final Map indicators; + private final ReactiveHealthIndicatorRegistry registry; private final HealthAggregator healthAggregator; @@ -47,15 +45,42 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator private final Function, Mono> timeoutCompose; + /** + * Create a new {@link CompositeReactiveHealthIndicator}. + * @param healthAggregator the health aggregator + * @deprecated since 2.1.0 in favour of + * {@link #CompositeReactiveHealthIndicator(HealthAggregator, ReactiveHealthIndicatorRegistry)} + */ + @Deprecated public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator) { this(healthAggregator, new LinkedHashMap<>()); } + /** + * Create a new {@link CompositeReactiveHealthIndicator} from the specified + * indicators. + * @param healthAggregator the health aggregator + * @param indicators a map of {@link ReactiveHealthIndicator HealthIndicators} with + * the key being used as an indicator name. + * @deprecated since 2.1.0 in favour of + * {@link #CompositeReactiveHealthIndicator(HealthAggregator, ReactiveHealthIndicatorRegistry)} + */ + @Deprecated public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator, Map indicators) { - Assert.notNull(healthAggregator, "HealthAggregator must not be null"); - Assert.notNull(indicators, "Indicators must not be null"); - this.indicators = new LinkedHashMap<>(indicators); + this(healthAggregator, new DefaultReactiveHealthIndicatorRegistry(indicators)); + + } + + /** + * Create a new {@link CompositeReactiveHealthIndicator} from the indicators in the + * given {@code registry}. + * @param healthAggregator the health aggregator + * @param registry the registry of {@link ReactiveHealthIndicator HealthIndicators}. + */ + public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator, + ReactiveHealthIndicatorRegistry registry) { + this.registry = registry; this.healthAggregator = healthAggregator; this.timeoutCompose = (mono) -> (this.timeout != null ? mono.timeout( Duration.ofMillis(this.timeout), Mono.just(this.timeoutHealth)) : mono); @@ -66,10 +91,15 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator * @param name the name of the health indicator * @param indicator the health indicator to add * @return this instance + * @throws IllegalStateException if an indicator with the given {@code name} + * is already registered. + * @deprecated since 2.1.0 in favour of + * {@link ReactiveHealthIndicatorRegistry#register(String, ReactiveHealthIndicator)} */ + @Deprecated public CompositeReactiveHealthIndicator addHealthIndicator(String name, ReactiveHealthIndicator indicator) { - this.indicators.put(name, indicator); + this.registry.register(name, indicator); return this; } @@ -92,7 +122,7 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator @Override public Mono health() { - return Flux.fromIterable(this.indicators.entrySet()) + return Flux.fromIterable(this.registry.getAll().entrySet()) .flatMap((entry) -> Mono.zip(Mono.just(entry.getKey()), entry.getValue().health().compose(this.timeoutCompose))) .collectMap(Tuple2::getT1, Tuple2::getT2) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactory.java index 5404460256..7fcf374622 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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,19 +16,20 @@ package org.springframework.boot.actuate.health; -import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Factory to create a {@link CompositeReactiveHealthIndicator}. * * @author Stephane Nicoll * @since 2.0.0 + * @deprecated since 2.1.0 in favor of + * {@link CompositeReactiveHealthIndicator#CompositeReactiveHealthIndicator(HealthAggregator, ReactiveHealthIndicatorRegistry)} */ +@Deprecated public class CompositeReactiveHealthIndicatorFactory { private final Function healthIndicatorNameFactory; @@ -62,30 +63,11 @@ public class CompositeReactiveHealthIndicatorFactory { Assert.notNull(healthAggregator, "HealthAggregator must not be null"); Assert.notNull(reactiveHealthIndicators, "ReactiveHealthIndicators must not be null"); - CompositeReactiveHealthIndicator healthIndicator = new CompositeReactiveHealthIndicator( - healthAggregator); - merge(reactiveHealthIndicators, healthIndicators) - .forEach((beanName, indicator) -> { - String name = this.healthIndicatorNameFactory.apply(beanName); - healthIndicator.addHealthIndicator(name, indicator); - }); - return healthIndicator; - } - - private Map merge( - Map reactiveHealthIndicators, - Map healthIndicators) { - if (ObjectUtils.isEmpty(healthIndicators)) { - return reactiveHealthIndicators; - } - Map allIndicators = new LinkedHashMap<>( - reactiveHealthIndicators); - healthIndicators.forEach((beanName, indicator) -> { - String name = this.healthIndicatorNameFactory.apply(beanName); - allIndicators.computeIfAbsent(name, - (n) -> new HealthIndicatorReactiveAdapter(indicator)); - }); - return allIndicators; + ReactiveHealthIndicatorRegistryFactory factory = new ReactiveHealthIndicatorRegistryFactory( + this.healthIndicatorNameFactory); + return new CompositeReactiveHealthIndicator(healthAggregator, + factory.createReactiveHealthIndicatorRegistry(reactiveHealthIndicators, + healthIndicators)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistry.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistry.java new file mode 100644 index 0000000000..1b9d9145eb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistry.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2018 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 + * + * http://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 java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveHealthIndicatorRegistry}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.1.0 + */ +public class DefaultReactiveHealthIndicatorRegistry + implements ReactiveHealthIndicatorRegistry { + + private final Object monitor = new Object(); + + private final Map healthIndicators; + + /** + * Create a new {@link DefaultReactiveHealthIndicatorRegistry}. + */ + public DefaultReactiveHealthIndicatorRegistry() { + this(new LinkedHashMap<>()); + } + + /** + * Create a new {@link DefaultReactiveHealthIndicatorRegistry} from the specified + * indicators. + * @param healthIndicators a map of {@link HealthIndicator}s with the key + * being used as an indicator name. + */ + public DefaultReactiveHealthIndicatorRegistry( + Map healthIndicators) { + Assert.notNull(healthIndicators, "HealthIndicators must not be null"); + this.healthIndicators = new LinkedHashMap<>(healthIndicators); + } + + @Override + public void register(String name, ReactiveHealthIndicator healthIndicator) { + Assert.notNull(healthIndicator, "HealthIndicator must not be null"); + Assert.notNull(name, "Name must not be null"); + synchronized (this.monitor) { + ReactiveHealthIndicator existing = this.healthIndicators.putIfAbsent(name, + healthIndicator); + if (existing != null) { + throw new IllegalStateException( + "HealthIndicator with name '" + name + "' already registered"); + } + } + } + + @Override + public ReactiveHealthIndicator unregister(String name) { + Assert.notNull(name, "Name must not be null"); + synchronized (this.monitor) { + return this.healthIndicators.remove(name); + } + } + + @Override + public ReactiveHealthIndicator get(String name) { + Assert.notNull(name, "Name must not be null"); + synchronized (this.monitor) { + return this.healthIndicators.get(name); + } + } + + @Override + public Map getAll() { + synchronized (this.monitor) { + return Collections + .unmodifiableMap(new LinkedHashMap<>(this.healthIndicators)); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java index cce483e4e9..b466b9479e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java @@ -31,7 +31,7 @@ import java.util.Map; public interface HealthIndicatorRegistry { /** - * Registers the given {@code healthIndicator}, associating it with the + * Registers the given {@link HealthIndicator}, associating it with the * given {@code name}. * @param name the name of the indicator * @param healthIndicator the indicator @@ -41,7 +41,7 @@ public interface HealthIndicatorRegistry { void register(String name, HealthIndicator healthIndicator); /** - * Unregisters the {@code HealthIndicator} previously registered with the + * Unregisters the {@link HealthIndicator} previously registered with the * given {@code name}. * @param name the name of the indicator * @return the unregistered indicator, or {@code null} if no indicator was @@ -50,7 +50,7 @@ public interface HealthIndicatorRegistry { HealthIndicator unregister(String name); /** - * Returns the health indicator registered with the given {@code name}. + * Returns the {@link HealthIndicator} registered with the given {@code name}. * @param name the name of the indicator * @return the health indicator, or {@code null} if no indicator was * registered with the given {@code name}. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistry.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistry.java new file mode 100644 index 0000000000..1121cd332c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistry.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.Map; + +/** + * A registry of {@link ReactiveHealthIndicator ReactiveHealthIndicators}. + *

+ * Implementations must be thread-safe. + * + * @author Andy Wilkinson + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.1.0 + */ +public interface ReactiveHealthIndicatorRegistry { + + /** + * Registers the given {@link ReactiveHealthIndicator}, associating it with the + * given {@code name}. + * @param name the name of the indicator + * @param healthIndicator the indicator + * @throws IllegalStateException if an indicator with the given {@code name} + * is already registered. + */ + void register(String name, ReactiveHealthIndicator healthIndicator); + + /** + * Unregisters the {@link ReactiveHealthIndicator} previously registered with the + * given {@code name}. + * @param name the name of the indicator + * @return the unregistered indicator, or {@code null} if no indicator was + * found in the registry for the given {@code name}. + */ + ReactiveHealthIndicator unregister(String name); + + /** + * Returns the {@link ReactiveHealthIndicator} registered with the given {@code name}. + * @param name the name of the indicator + * @return the health indicator, or {@code null} if no indicator was + * registered with the given {@code name}. + */ + ReactiveHealthIndicator get(String name); + + /** + * Returns a snapshot of the registered health indicators and their names. + * The contents of the map do not reflect subsequent changes to the + * registry. + * @return the snapshot of registered health indicators + */ + Map getAll(); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactory.java new file mode 100644 index 0000000000..c4a69a8edf --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactory.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2018 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 + * + * http://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; +import java.util.function.Function; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Factory to create a {@link HealthIndicatorRegistry}. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +public class ReactiveHealthIndicatorRegistryFactory { + + private final Function healthIndicatorNameFactory; + + public ReactiveHealthIndicatorRegistryFactory( + Function healthIndicatorNameFactory) { + this.healthIndicatorNameFactory = healthIndicatorNameFactory; + } + + public ReactiveHealthIndicatorRegistryFactory() { + this(new HealthIndicatorNameFactory()); + } + + /** + * Create a {@link ReactiveHealthIndicatorRegistry} based on the specified health + * indicators. Each {@link HealthIndicator} are wrapped to a + * {@link HealthIndicatorReactiveAdapter}. If two instances share the same name, the + * reactive variant takes precedence. + * @param reactiveHealthIndicators the {@link ReactiveHealthIndicator} instances + * mapped by name + * @param healthIndicators the {@link HealthIndicator} instances mapped by name if + * any. + * @return a {@link ReactiveHealthIndicator} that delegates to the specified + * {@code reactiveHealthIndicators}. + */ + public ReactiveHealthIndicatorRegistry createReactiveHealthIndicatorRegistry( + Map reactiveHealthIndicators, + Map healthIndicators) { + Assert.notNull(reactiveHealthIndicators, + "ReactiveHealthIndicators must not be null"); + return initialize(new DefaultReactiveHealthIndicatorRegistry(), + reactiveHealthIndicators, healthIndicators); + } + + protected T initialize(T registry, + Map reactiveHealthIndicators, + Map healthIndicators) { + merge(reactiveHealthIndicators, healthIndicators) + .forEach((beanName, indicator) -> { + String name = this.healthIndicatorNameFactory.apply(beanName); + registry.register(name, indicator); + }); + return registry; + } + + private Map merge( + Map reactiveHealthIndicators, + Map healthIndicators) { + if (ObjectUtils.isEmpty(healthIndicators)) { + return reactiveHealthIndicators; + } + Map allIndicators = new LinkedHashMap<>( + reactiveHealthIndicators); + healthIndicators.forEach((beanName, indicator) -> { + String name = this.healthIndicatorNameFactory.apply(beanName); + allIndicators.computeIfAbsent(name, + (n) -> new HealthIndicatorReactiveAdapter(indicator)); + }); + return allIndicators; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactoryTests.java index 1eff7355b4..09f616c36b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorFactoryTests.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verify; * * @author Stephane Nicoll */ +@Deprecated public class CompositeReactiveHealthIndicatorFactoryTests { private static final Health UP = new Health.Builder().status(Status.UP).build(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java index e9d1b06cea..7e63f176d0 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -17,6 +17,9 @@ package org.springframework.boot.actuate.health; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; import reactor.core.publisher.Mono; @@ -38,13 +41,12 @@ public class CompositeReactiveHealthIndicatorTests { private OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator(); - private CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator( - this.healthAggregator); - @Test public void singleIndicator() { - this.indicator.addHealthIndicator("test", () -> Mono.just(HEALTHY)); - StepVerifier.create(this.indicator.health()).consumeNextWith((h) -> { + CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator( + this.healthAggregator, new DefaultReactiveHealthIndicatorRegistry( + Collections.singletonMap("test", () -> Mono.just(HEALTHY)))); + StepVerifier.create(indicator.health()).consumeNextWith((h) -> { assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getDetails()).containsOnlyKeys("test"); assertThat(h.getDetails().get("test")).isEqualTo(HEALTHY); @@ -53,24 +55,31 @@ public class CompositeReactiveHealthIndicatorTests { @Test public void longHealth() { + Map indicators = new HashMap<>(); for (int i = 0; i < 50; i++) { - this.indicator.addHealthIndicator("test" + i, - new TimeoutHealth(10000, Status.UP)); + indicators.put("test" + i, new TimeoutHealth(10000, Status.UP)); } - StepVerifier.withVirtualTime(this.indicator::health).expectSubscription() + CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator( + this.healthAggregator, + new DefaultReactiveHealthIndicatorRegistry(indicators)); + StepVerifier.withVirtualTime(indicator::health).expectSubscription() .thenAwait(Duration.ofMillis(10000)).consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).hasSize(50); - }).verifyComplete(); + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).hasSize(50); + }).verifyComplete(); } @Test public void timeoutReachedUsesFallback() { - this.indicator.addHealthIndicator("slow", new TimeoutHealth(10000, Status.UP)) - .addHealthIndicator("fast", new TimeoutHealth(10, Status.UP)) + Map indicators = new HashMap<>(); + indicators.put("slow", new TimeoutHealth(10000, Status.UP)); + indicators.put("fast", new TimeoutHealth(10, Status.UP)); + CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator( + this.healthAggregator, + new DefaultReactiveHealthIndicatorRegistry(indicators)) .timeoutStrategy(100, UNKNOWN_HEALTH); - StepVerifier.create(this.indicator.health()).consumeNextWith((h) -> { + StepVerifier.create(indicator.health()).consumeNextWith((h) -> { assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); assertThat(h.getDetails().get("slow")).isEqualTo(UNKNOWN_HEALTH); @@ -80,16 +89,20 @@ public class CompositeReactiveHealthIndicatorTests { @Test public void timeoutNotReached() { - this.indicator.addHealthIndicator("slow", new TimeoutHealth(10000, Status.UP)) - .addHealthIndicator("fast", new TimeoutHealth(10, Status.UP)) + Map indicators = new HashMap<>(); + indicators.put("slow", new TimeoutHealth(10000, Status.UP)); + indicators.put("fast", new TimeoutHealth(10, Status.UP)); + CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator( + this.healthAggregator, + new DefaultReactiveHealthIndicatorRegistry(indicators)) .timeoutStrategy(20000, null); - StepVerifier.withVirtualTime(this.indicator::health).expectSubscription() + StepVerifier.withVirtualTime(indicator::health).expectSubscription() .thenAwait(Duration.ofMillis(10000)).consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); - assertThat(h.getDetails().get("slow")).isEqualTo(HEALTHY); - assertThat(h.getDetails().get("fast")).isEqualTo(HEALTHY); - }).verifyComplete(); + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); + assertThat(h.getDetails().get("slow")).isEqualTo(HEALTHY); + assertThat(h.getDetails().get("fast")).isEqualTo(HEALTHY); + }).verifyComplete(); } static class TimeoutHealth implements ReactiveHealthIndicator { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistryTests.java new file mode 100644 index 0000000000..e8b4adc66c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistryTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.Map; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultReactiveHealthIndicatorRegistry}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +public class DefaultReactiveHealthIndicatorRegistryTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private ReactiveHealthIndicator one = mock(ReactiveHealthIndicator.class); + + private ReactiveHealthIndicator two = mock(ReactiveHealthIndicator.class); + + private DefaultReactiveHealthIndicatorRegistry registry; + + @Before + public void setUp() { + given(this.one.health()).willReturn(Mono.just( + new Health.Builder().unknown().withDetail("1", "1").build())); + given(this.two.health()).willReturn(Mono.just( + new Health.Builder().unknown().withDetail("2", "2").build())); + this.registry = new DefaultReactiveHealthIndicatorRegistry(); + } + + @Test + public void register() { + this.registry.register("one", this.one); + this.registry.register("two", this.two); + assertThat(this.registry.getAll()).hasSize(2); + assertThat(this.registry.get("one")).isSameAs(this.one); + assertThat(this.registry.get("two")).isSameAs(this.two); + } + + @Test + public void registerAlreadyUsedName() { + this.registry.register("one", this.one); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("HealthIndicator with name 'one' already registered"); + this.registry.register("one", this.two); + } + + @Test + public void unregister() { + this.registry.register("one", this.one); + this.registry.register("two", this.two); + assertThat(this.registry.getAll()).hasSize(2); + ReactiveHealthIndicator two = this.registry.unregister("two"); + assertThat(two).isSameAs(this.two); + assertThat(this.registry.getAll()).hasSize(1); + } + + @Test + public void unregisterUnknown() { + this.registry.register("one", this.one); + assertThat(this.registry.getAll()).hasSize(1); + ReactiveHealthIndicator two = this.registry.unregister("two"); + assertThat(two).isNull(); + assertThat(this.registry.getAll()).hasSize(1); + } + + @Test + public void getAllIsASnapshot() { + this.registry.register("one", this.one); + Map snapshot = this.registry.getAll(); + assertThat(snapshot).containsOnlyKeys("one"); + this.registry.register("two", this.two); + assertThat(snapshot).containsOnlyKeys("one"); + } + + @Test + public void getAllIsImmutable() { + this.registry.register("one", this.one); + Map snapshot = this.registry.getAll(); + + this.thrown.expect(UnsupportedOperationException.class); + snapshot.clear(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactoryTests.java new file mode 100644 index 0000000000..824d0d6180 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactoryTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReactiveHealthIndicatorRegistryFactory}. + * + * @author Stephane Nicoll + */ +public class ReactiveHealthIndicatorRegistryFactoryTests { + + private static final Health UP = new Health.Builder().status(Status.UP).build(); + + private static final Health DOWN = new Health.Builder().status(Status.DOWN).build(); + + private final ReactiveHealthIndicatorRegistryFactory factory = new ReactiveHealthIndicatorRegistryFactory(); + + @Test + public void defaultHealthIndicatorNameFactory() { + ReactiveHealthIndicatorRegistry registry = this.factory.createReactiveHealthIndicatorRegistry( + Collections.singletonMap("myHealthIndicator", () -> Mono.just(UP)), null); + assertThat(registry.getAll()).containsOnlyKeys("my"); + } + + @Test + public void healthIndicatorIsAdapted() { + ReactiveHealthIndicatorRegistry registry = this.factory.createReactiveHealthIndicatorRegistry( + Collections.singletonMap("test", () -> Mono.just(UP)), + Collections.singletonMap("regular", () -> DOWN)); + assertThat(registry.getAll()).containsOnlyKeys("test", "regular"); + StepVerifier.create(registry.get("regular").health()).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.DOWN); + assertThat(h.getDetails()).isEmpty(); + }).verifyComplete(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index c46d089644..56f22c78ef 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -725,16 +725,20 @@ NOTE: If you have secured your application and wish to use `always`, your securi configuration must permit access to the health endpoint for both authenticated and unauthenticated users. -Health information is collected from all +Health information is collected from the content of a +{sc-spring-boot-actuator}/health/HealthIndicatorRegistry.{sc-ext}[ +`HealthIndicatorRegistry`] (by default all {sc-spring-boot-actuator}/health/HealthIndicator.{sc-ext}[`HealthIndicator`] instances -registered with {sc-spring-boot-actuator}/health/HealthIndicatorRegistry.{sc-ext}[ -`HealthIndicatorRegistry`]. Spring Boot includes a number of auto-configured +defined in your `ApplicationContext`. Spring Boot includes a number of auto-configured `HealthIndicators` and you can also write your own. By default, the final system state is derived by the `HealthAggregator` which sorts the statuses from each `HealthIndicator` based on an ordered list of statuses. The first status in the sorted list is used as the overall health status. If no `HealthIndicator` returns a status that is known to the `HealthAggregator`, an `UNKNOWN` status is used. +TIP: The `HealthIndicatorRegistry` can be used to register and unregister health +indicators at runtime. + ==== Auto-configured HealthIndicators @@ -819,10 +823,6 @@ NOTE: The identifier for a given `HealthIndicator` is the name of the bean witho `HealthIndicator` suffix, if it exists. In the preceding example, the health information is available in an entry named `my`. -Additionally, you can register (and unregister) `HealthIndicator` instances in runtime -using {sc-spring-boot-actuator}/health/HealthIndicatorRegistry.{sc-ext}[ -`HealthIndicatorRegistry`]. - In addition to Spring Boot's predefined {sc-spring-boot-actuator}/health/Status.{sc-ext}[`Status`] types, it is also possible for `Health` to return a custom `Status` that represents a new system state. In such cases, a @@ -877,10 +877,17 @@ The following table shows the default status mappings for the built-in statuses: ==== Reactive Health Indicators For reactive applications, such as those using Spring WebFlux, `ReactiveHealthIndicator` provides a non-blocking contract for getting application health. Similar to a traditional -`HealthIndicator`, health information is collected from all -{sc-spring-boot-actuator}/health/ReactiveHealthIndicator.{sc-ext}[`ReactiveHealthIndicator`] -beans defined in your `ApplicationContext`. Regular `HealthIndicator` beans that do not -check against a reactive API are included and executed on the elastic scheduler. +`HealthIndicator`, health information is collected from the content of a +{sc-spring-boot-actuator}/health/ReactiveHealthIndicatorRegistry.{sc-ext}[ +`ReactiveHealthIndicatorRegistry`] (by default all +{sc-spring-boot-actuator}/health/HealthIndicator.{sc-ext}[`HealthIndicator`] and +{sc-spring-boot-actuator}/health/ReactiveHealthIndicator.{sc-ext}[ +`ReactiveHealthIndicator`] instances defined in your `ApplicationContext`. Regular +`HealthIndicator` that do not check against a reactive API are executed on the elastic +scheduler. + +TIP: In a reactive application, The `ReactiveHealthIndicatorRegistry` can be used to +register and unregister health indicators at runtime. To provide custom health information from a reactive API, you can register Spring beans that implement the