Add support for ReactiveHealthIndicatorRegistry

This commit updates the initial proposal to add support for reactive
use cases as well. A reactive application can use
ReactiveHealthIndicatorRegistry as an alternative to
HealthIndicatorRegistry.

Closes gh-4965
pull/13197/head
Stephane Nicoll 7 years ago
parent 95b251590e
commit 2c176a3770

@ -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<H extends Re
if (beans.size() == 1) {
return createHealthIndicator(beans.values().iterator().next());
}
CompositeReactiveHealthIndicator composite = new CompositeReactiveHealthIndicator(
this.healthAggregator);
beans.forEach((name, source) -> 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")

@ -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> healthAggregator,
ObjectProvider<Map<String, ReactiveHealthIndicator>> reactiveHealthIndicators,
ObjectProvider<Map<String, HealthIndicator>> 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

@ -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<Map<String, ReactiveHealthIndicator>> reactiveHealthIndicators,
ObjectProvider<Map<String, HealthIndicator>> healthIndicators) {
return new ReactiveHealthIndicatorRegistryFactory().createReactiveHealthIndicatorRegistry(
reactiveHealthIndicators.getIfAvailable(Collections::emptyMap),
healthIndicators.getIfAvailable(Collections::emptyMap));
}
}
}

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

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

@ -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<String, ReactiveHealthIndicator> indicators;
private final ReactiveHealthIndicatorRegistry registry;
private final HealthAggregator healthAggregator;
@ -47,15 +45,42 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator
private final Function<Mono<Health>, Mono<Health>> 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<String, ReactiveHealthIndicator> 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> 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)

@ -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<String, String> 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<String, ReactiveHealthIndicator> merge(
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
Map<String, HealthIndicator> healthIndicators) {
if (ObjectUtils.isEmpty(healthIndicators)) {
return reactiveHealthIndicators;
}
Map<String, ReactiveHealthIndicator> 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));
}
}

@ -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<String, ReactiveHealthIndicator> 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<String, ReactiveHealthIndicator> 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<String, ReactiveHealthIndicator> getAll() {
synchronized (this.monitor) {
return Collections
.unmodifiableMap(new LinkedHashMap<>(this.healthIndicators));
}
}
}

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

@ -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}.
* <p>
* Implementations <strong>must</strong> 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<String, ReactiveHealthIndicator> getAll();
}

@ -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<String, String> healthIndicatorNameFactory;
public ReactiveHealthIndicatorRegistryFactory(
Function<String, String> 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<String, ReactiveHealthIndicator> reactiveHealthIndicators,
Map<String, HealthIndicator> healthIndicators) {
Assert.notNull(reactiveHealthIndicators,
"ReactiveHealthIndicators must not be null");
return initialize(new DefaultReactiveHealthIndicatorRegistry(),
reactiveHealthIndicators, healthIndicators);
}
protected <T extends ReactiveHealthIndicatorRegistry> T initialize(T registry,
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
Map<String, HealthIndicator> healthIndicators) {
merge(reactiveHealthIndicators, healthIndicators)
.forEach((beanName, indicator) -> {
String name = this.healthIndicatorNameFactory.apply(beanName);
registry.register(name, indicator);
});
return registry;
}
private Map<String, ReactiveHealthIndicator> merge(
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
Map<String, HealthIndicator> healthIndicators) {
if (ObjectUtils.isEmpty(healthIndicators)) {
return reactiveHealthIndicators;
}
Map<String, ReactiveHealthIndicator> allIndicators = new LinkedHashMap<>(
reactiveHealthIndicators);
healthIndicators.forEach((beanName, indicator) -> {
String name = this.healthIndicatorNameFactory.apply(beanName);
allIndicators.computeIfAbsent(name,
(n) -> new HealthIndicatorReactiveAdapter(indicator));
});
return allIndicators;
}
}

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

@ -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<String, ReactiveHealthIndicator> 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<String, ReactiveHealthIndicator> 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<String, ReactiveHealthIndicator> 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 {

@ -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<String, ReactiveHealthIndicator> 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<String, ReactiveHealthIndicator> snapshot = this.registry.getAll();
this.thrown.expect(UnsupportedOperationException.class);
snapshot.clear();
}
}

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

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

Loading…
Cancel
Save