Commit 2c176a37 authored by Stephane Nicoll's avatar Stephane Nicoll

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
parent 95b25159
...@@ -20,8 +20,10 @@ import java.util.Map; ...@@ -20,8 +20,10 @@ import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator; 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.HealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
/** /**
...@@ -41,11 +43,10 @@ public abstract class CompositeReactiveHealthIndicatorConfiguration<H extends Re ...@@ -41,11 +43,10 @@ public abstract class CompositeReactiveHealthIndicatorConfiguration<H extends Re
if (beans.size() == 1) { if (beans.size() == 1) {
return createHealthIndicator(beans.values().iterator().next()); return createHealthIndicator(beans.values().iterator().next());
} }
CompositeReactiveHealthIndicator composite = new CompositeReactiveHealthIndicator( ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry();
this.healthAggregator); beans.forEach((name, source) -> registry.register(name,
beans.forEach((name, source) -> composite.addHealthIndicator(name,
createHealthIndicator(source))); createHealthIndicator(source)));
return composite; return new CompositeReactiveHealthIndicator(this.healthAggregator, registry);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
......
...@@ -16,23 +16,21 @@ ...@@ -16,23 +16,21 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collections;
import java.util.Map;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; 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.HealthAggregator;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension; 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.HealthStatusHttpMapper;
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper; import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.OrderedHealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; 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.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
...@@ -70,19 +68,16 @@ class HealthEndpointWebExtensionConfiguration { ...@@ -70,19 +68,16 @@ class HealthEndpointWebExtensionConfiguration {
@Configuration @Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE) @ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnSingleCandidate(ReactiveHealthIndicatorRegistry.class)
static class ReactiveWebHealthConfiguration { static class ReactiveWebHealthConfiguration {
private final ReactiveHealthIndicator reactiveHealthIndicator; private final ReactiveHealthIndicator reactiveHealthIndicator;
ReactiveWebHealthConfiguration(ObjectProvider<HealthAggregator> healthAggregator, ReactiveWebHealthConfiguration(ObjectProvider<HealthAggregator> healthAggregator,
ObjectProvider<Map<String, ReactiveHealthIndicator>> reactiveHealthIndicators, ReactiveHealthIndicatorRegistry registry) {
ObjectProvider<Map<String, HealthIndicator>> healthIndicators) { this.reactiveHealthIndicator = new CompositeReactiveHealthIndicator(
this.reactiveHealthIndicator = new CompositeReactiveHealthIndicatorFactory()
.createReactiveHealthIndicator(
healthAggregator.getIfAvailable(OrderedHealthAggregator::new), healthAggregator.getIfAvailable(OrderedHealthAggregator::new),
reactiveHealthIndicators registry);
.getIfAvailable(Collections::emptyMap),
healthIndicators.getIfAvailable(Collections::emptyMap));
} }
@Bean @Bean
......
...@@ -16,13 +16,22 @@ ...@@ -16,13 +16,22 @@
package org.springframework.boot.actuate.autoconfigure.health; 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.ApplicationHealthIndicator;
import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.actuate.health.HealthIndicatorRegistry;
import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.OrderedHealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; 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.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
...@@ -71,4 +80,20 @@ public class HealthIndicatorAutoConfiguration { ...@@ -71,4 +80,20 @@ public class HealthIndicatorAutoConfiguration {
return HealthIndicatorRegistryBeans.get(applicationContext); 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));
}
}
} }
/*
* 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; ...@@ -28,6 +28,7 @@ import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper; import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; 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.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -217,6 +218,24 @@ public class ReactiveHealthEndpointWebExtensionTests { ...@@ -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 @Configuration
static class HealthIndicatorsConfiguration { static class HealthIndicatorsConfiguration {
......
...@@ -25,8 +25,6 @@ import reactor.core.publisher.Flux; ...@@ -25,8 +25,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2; import reactor.util.function.Tuple2;
import org.springframework.util.Assert;
/** /**
* {@link ReactiveHealthIndicator} that returns health indications from all registered * {@link ReactiveHealthIndicator} that returns health indications from all registered
* delegates. Provides an alternative {@link Health} for a delegate that reaches a * delegates. Provides an alternative {@link Health} for a delegate that reaches a
...@@ -37,7 +35,7 @@ import org.springframework.util.Assert; ...@@ -37,7 +35,7 @@ import org.springframework.util.Assert;
*/ */
public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator { public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator {
private final Map<String, ReactiveHealthIndicator> indicators; private final ReactiveHealthIndicatorRegistry registry;
private final HealthAggregator healthAggregator; private final HealthAggregator healthAggregator;
...@@ -47,15 +45,42 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator ...@@ -47,15 +45,42 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator
private final Function<Mono<Health>, Mono<Health>> timeoutCompose; 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) { public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator) {
this(healthAggregator, new LinkedHashMap<>()); 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, public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator,
Map<String, ReactiveHealthIndicator> indicators) { Map<String, ReactiveHealthIndicator> indicators) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null"); this(healthAggregator, new DefaultReactiveHealthIndicatorRegistry(indicators));
Assert.notNull(indicators, "Indicators must not be null");
this.indicators = new LinkedHashMap<>(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.healthAggregator = healthAggregator;
this.timeoutCompose = (mono) -> (this.timeout != null ? mono.timeout( this.timeoutCompose = (mono) -> (this.timeout != null ? mono.timeout(
Duration.ofMillis(this.timeout), Mono.just(this.timeoutHealth)) : mono); Duration.ofMillis(this.timeout), Mono.just(this.timeoutHealth)) : mono);
...@@ -66,10 +91,15 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator ...@@ -66,10 +91,15 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator
* @param name the name of the health indicator * @param name the name of the health indicator
* @param indicator the health indicator to add * @param indicator the health indicator to add
* @return this instance * @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, public CompositeReactiveHealthIndicator addHealthIndicator(String name,
ReactiveHealthIndicator indicator) { ReactiveHealthIndicator indicator) {
this.indicators.put(name, indicator); this.registry.register(name, indicator);
return this; return this;
} }
...@@ -92,7 +122,7 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator ...@@ -92,7 +122,7 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator
@Override @Override
public Mono<Health> health() { 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()), .flatMap((entry) -> Mono.zip(Mono.just(entry.getKey()),
entry.getValue().health().compose(this.timeoutCompose))) entry.getValue().health().compose(this.timeoutCompose)))
.collectMap(Tuple2::getT1, Tuple2::getT2) .collectMap(Tuple2::getT1, Tuple2::getT2)
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,19 +16,20 @@ ...@@ -16,19 +16,20 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/** /**
* Factory to create a {@link CompositeReactiveHealthIndicator}. * Factory to create a {@link CompositeReactiveHealthIndicator}.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 2.0.0 * @since 2.0.0
* @deprecated since 2.1.0 in favor of
* {@link CompositeReactiveHealthIndicator#CompositeReactiveHealthIndicator(HealthAggregator, ReactiveHealthIndicatorRegistry)}
*/ */
@Deprecated
public class CompositeReactiveHealthIndicatorFactory { public class CompositeReactiveHealthIndicatorFactory {
private final Function<String, String> healthIndicatorNameFactory; private final Function<String, String> healthIndicatorNameFactory;
...@@ -62,30 +63,11 @@ public class CompositeReactiveHealthIndicatorFactory { ...@@ -62,30 +63,11 @@ public class CompositeReactiveHealthIndicatorFactory {
Assert.notNull(healthAggregator, "HealthAggregator must not be null"); Assert.notNull(healthAggregator, "HealthAggregator must not be null");
Assert.notNull(reactiveHealthIndicators, Assert.notNull(reactiveHealthIndicators,
"ReactiveHealthIndicators must not be null"); "ReactiveHealthIndicators must not be null");
CompositeReactiveHealthIndicator healthIndicator = new CompositeReactiveHealthIndicator( ReactiveHealthIndicatorRegistryFactory factory = new ReactiveHealthIndicatorRegistryFactory(
healthAggregator); this.healthIndicatorNameFactory);
merge(reactiveHealthIndicators, healthIndicators) return new CompositeReactiveHealthIndicator(healthAggregator,
.forEach((beanName, indicator) -> { factory.createReactiveHealthIndicatorRegistry(reactiveHealthIndicators,
String name = this.healthIndicatorNameFactory.apply(beanName); healthIndicators));
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;
} }
} }
/*
* 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; ...@@ -31,7 +31,7 @@ import java.util.Map;
public interface HealthIndicatorRegistry { public interface HealthIndicatorRegistry {
/** /**
* Registers the given {@code healthIndicator}, associating it with the * Registers the given {@link HealthIndicator}, associating it with the
* given {@code name}. * given {@code name}.
* @param name the name of the indicator * @param name the name of the indicator
* @param healthIndicator the indicator * @param healthIndicator the indicator
...@@ -41,7 +41,7 @@ public interface HealthIndicatorRegistry { ...@@ -41,7 +41,7 @@ public interface HealthIndicatorRegistry {
void register(String name, HealthIndicator healthIndicator); 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}. * given {@code name}.
* @param name the name of the indicator * @param name the name of the indicator
* @return the unregistered indicator, or {@code null} if no indicator was * @return the unregistered indicator, or {@code null} if no indicator was
...@@ -50,7 +50,7 @@ public interface HealthIndicatorRegistry { ...@@ -50,7 +50,7 @@ public interface HealthIndicatorRegistry {
HealthIndicator unregister(String name); 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 * @param name the name of the indicator
* @return the health indicator, or {@code null} if no indicator was * @return the health indicator, or {@code null} if no indicator was
* registered with the given {@code name}. * registered with the given {@code name}.
......
/*
* 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();
}
/*
* 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; ...@@ -35,6 +35,7 @@ import static org.mockito.Mockito.verify;
* *
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
@Deprecated
public class CompositeReactiveHealthIndicatorFactoryTests { public class CompositeReactiveHealthIndicatorFactoryTests {
private static final Health UP = new Health.Builder().status(Status.UP).build(); private static final Health UP = new Health.Builder().status(Status.UP).build();
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
...@@ -38,13 +41,12 @@ public class CompositeReactiveHealthIndicatorTests { ...@@ -38,13 +41,12 @@ public class CompositeReactiveHealthIndicatorTests {
private OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator(); private OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator();
private CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator(
this.healthAggregator);
@Test @Test
public void singleIndicator() { public void singleIndicator() {
this.indicator.addHealthIndicator("test", () -> Mono.just(HEALTHY)); CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator(
StepVerifier.create(this.indicator.health()).consumeNextWith((h) -> { this.healthAggregator, new DefaultReactiveHealthIndicatorRegistry(
Collections.singletonMap("test", () -> Mono.just(HEALTHY))));
StepVerifier.create(indicator.health()).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).containsOnlyKeys("test"); assertThat(h.getDetails()).containsOnlyKeys("test");
assertThat(h.getDetails().get("test")).isEqualTo(HEALTHY); assertThat(h.getDetails().get("test")).isEqualTo(HEALTHY);
...@@ -53,11 +55,14 @@ public class CompositeReactiveHealthIndicatorTests { ...@@ -53,11 +55,14 @@ public class CompositeReactiveHealthIndicatorTests {
@Test @Test
public void longHealth() { public void longHealth() {
Map<String, ReactiveHealthIndicator> indicators = new HashMap<>();
for (int i = 0; i < 50; i++) { for (int i = 0; i < 50; i++) {
this.indicator.addHealthIndicator("test" + i, indicators.put("test" + i, new TimeoutHealth(10000, Status.UP));
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) -> { .thenAwait(Duration.ofMillis(10000)).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).hasSize(50); assertThat(h.getDetails()).hasSize(50);
...@@ -67,10 +72,14 @@ public class CompositeReactiveHealthIndicatorTests { ...@@ -67,10 +72,14 @@ public class CompositeReactiveHealthIndicatorTests {
@Test @Test
public void timeoutReachedUsesFallback() { public void timeoutReachedUsesFallback() {
this.indicator.addHealthIndicator("slow", new TimeoutHealth(10000, Status.UP)) Map<String, ReactiveHealthIndicator> indicators = new HashMap<>();
.addHealthIndicator("fast", new TimeoutHealth(10, Status.UP)) 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); .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.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); assertThat(h.getDetails()).containsOnlyKeys("slow", "fast");
assertThat(h.getDetails().get("slow")).isEqualTo(UNKNOWN_HEALTH); assertThat(h.getDetails().get("slow")).isEqualTo(UNKNOWN_HEALTH);
...@@ -80,10 +89,14 @@ public class CompositeReactiveHealthIndicatorTests { ...@@ -80,10 +89,14 @@ public class CompositeReactiveHealthIndicatorTests {
@Test @Test
public void timeoutNotReached() { public void timeoutNotReached() {
this.indicator.addHealthIndicator("slow", new TimeoutHealth(10000, Status.UP)) Map<String, ReactiveHealthIndicator> indicators = new HashMap<>();
.addHealthIndicator("fast", new TimeoutHealth(10, Status.UP)) 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); .timeoutStrategy(20000, null);
StepVerifier.withVirtualTime(this.indicator::health).expectSubscription() StepVerifier.withVirtualTime(indicator::health).expectSubscription()
.thenAwait(Duration.ofMillis(10000)).consumeNextWith((h) -> { .thenAwait(Duration.ofMillis(10000)).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); assertThat(h.getDetails()).containsOnlyKeys("slow", "fast");
......
/*
* 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();
}
}
/*
* 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 ...@@ -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 configuration must permit access to the health endpoint for both authenticated and
unauthenticated users. 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 {sc-spring-boot-actuator}/health/HealthIndicator.{sc-ext}[`HealthIndicator`] instances
registered with {sc-spring-boot-actuator}/health/HealthIndicatorRegistry.{sc-ext}[ defined in your `ApplicationContext`. Spring Boot includes a number of auto-configured
`HealthIndicatorRegistry`]. Spring Boot includes a number of auto-configured
`HealthIndicators` and you can also write your own. By default, the final system state is `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` 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 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 overall health status. If no `HealthIndicator` returns a status that is known to the
`HealthAggregator`, an `UNKNOWN` status is used. `HealthAggregator`, an `UNKNOWN` status is used.
TIP: The `HealthIndicatorRegistry` can be used to register and unregister health
indicators at runtime.
==== Auto-configured HealthIndicators ==== Auto-configured HealthIndicators
...@@ -819,10 +823,6 @@ NOTE: The identifier for a given `HealthIndicator` is the name of the bean witho ...@@ -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 `HealthIndicator` suffix, if it exists. In the preceding example, the health information
is available in an entry named `my`. 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 In addition to Spring Boot's predefined
{sc-spring-boot-actuator}/health/Status.{sc-ext}[`Status`] types, it is also possible for {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 `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: ...@@ -877,10 +877,17 @@ The following table shows the default status mappings for the built-in statuses:
==== Reactive Health Indicators ==== Reactive Health Indicators
For reactive applications, such as those using Spring WebFlux, `ReactiveHealthIndicator` For reactive applications, such as those using Spring WebFlux, `ReactiveHealthIndicator`
provides a non-blocking contract for getting application health. Similar to a traditional provides a non-blocking contract for getting application health. Similar to a traditional
`HealthIndicator`, health information is collected from all `HealthIndicator`, health information is collected from the content of a
{sc-spring-boot-actuator}/health/ReactiveHealthIndicator.{sc-ext}[`ReactiveHealthIndicator`] {sc-spring-boot-actuator}/health/ReactiveHealthIndicatorRegistry.{sc-ext}[
beans defined in your `ApplicationContext`. Regular `HealthIndicator` beans that do not `ReactiveHealthIndicatorRegistry`] (by default all
check against a reactive API are included and executed on the elastic scheduler. {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 To provide custom health information from a reactive API, you can register Spring beans
that implement the that implement the
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment