Skip to content

Commit 43144b6

Browse files
committed
Add support for health indicator groups
This commit allows a user to define an arbitrary number of health indicator groups using configuration. If a given health indicator is defined in more than one group, a single invocation is ensured using a AggregateHealth. A reactive counter-part is also provided. See spring-projectsgh-14022
1 parent c7d3b7a commit 43144b6

21 files changed

+1161
-31
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
package org.springframework.boot.actuate.autoconfigure.health;
1818

1919
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
20-
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
2120
import org.springframework.boot.actuate.health.HealthAggregator;
2221
import org.springframework.boot.actuate.health.HealthEndpoint;
2322
import org.springframework.boot.actuate.health.HealthIndicatorRegistry;
23+
import org.springframework.boot.actuate.health.OverallHealthIndicator;
2424
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
2626
import org.springframework.context.annotation.Bean;
@@ -39,7 +39,7 @@ class HealthEndpointConfiguration {
3939
@Bean
4040
@ConditionalOnMissingBean
4141
public HealthEndpoint healthEndpoint(HealthAggregator healthAggregator, HealthIndicatorRegistry registry) {
42-
return new HealthEndpoint(new CompositeHealthIndicator(healthAggregator, registry));
42+
return new HealthEndpoint(new OverallHealthIndicator(healthAggregator, registry));
4343
}
4444

4545
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.health;
1818

19+
import java.util.Map;
20+
1921
import org.springframework.beans.factory.ObjectProvider;
2022
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
21-
import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator;
2223
import org.springframework.boot.actuate.health.HealthAggregator;
2324
import org.springframework.boot.actuate.health.HealthEndpoint;
2425
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
2526
import org.springframework.boot.actuate.health.HealthStatusHttpMapper;
2627
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
2728
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
29+
import org.springframework.boot.actuate.health.OverallReactiveHealthIndicator;
2830
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
2931
import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry;
3032
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -48,10 +50,11 @@ class HealthEndpointWebExtensionConfiguration {
4850

4951
@Bean
5052
@ConditionalOnMissingBean
51-
public HealthStatusHttpMapper createHealthStatusHttpMapper(HealthIndicatorProperties healthIndicatorProperties) {
53+
public HealthStatusHttpMapper createHealthStatusHttpMapper(HealthIndicatorProperties properties) {
5254
HealthStatusHttpMapper statusHttpMapper = new HealthStatusHttpMapper();
53-
if (healthIndicatorProperties.getHttpMapping() != null) {
54-
statusHttpMapper.addStatusMapping(healthIndicatorProperties.getHttpMapping());
55+
Map<String, Integer> httpMapping = properties.getStatus().getHttpMapping();
56+
if (httpMapping != null) {
57+
statusHttpMapper.addStatusMapping(httpMapping);
5558
}
5659
return statusHttpMapper;
5760
}
@@ -75,7 +78,7 @@ static class ReactiveWebHealthConfiguration {
7578
public ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension(
7679
ObjectProvider<HealthAggregator> healthAggregator, ReactiveHealthIndicatorRegistry registry,
7780
HealthWebEndpointResponseMapper responseMapper) {
78-
return new ReactiveHealthEndpointWebExtension(new CompositeReactiveHealthIndicator(
81+
return new ReactiveHealthEndpointWebExtension(new OverallReactiveHealthIndicator(
7982
healthAggregator.getIfAvailable(OrderedHealthAggregator::new), registry), responseMapper);
8083
}
8184

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.health;
1818

19+
import java.util.LinkedHashMap;
20+
import java.util.LinkedHashSet;
1921
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.function.Function;
2024

2125
import reactor.core.publisher.Flux;
2226

2327
import org.springframework.boot.actuate.health.ApplicationHealthIndicator;
28+
import org.springframework.boot.actuate.health.GroupHealthIndicator;
29+
import org.springframework.boot.actuate.health.GroupReactiveHealthIndicator;
2430
import org.springframework.boot.actuate.health.HealthAggregator;
2531
import org.springframework.boot.actuate.health.HealthIndicator;
2632
import org.springframework.boot.actuate.health.HealthIndicatorRegistry;
@@ -59,16 +65,40 @@ public ApplicationHealthIndicator applicationHealthIndicator() {
5965
@ConditionalOnMissingBean(HealthAggregator.class)
6066
public OrderedHealthAggregator healthAggregator(HealthIndicatorProperties properties) {
6167
OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator();
62-
if (properties.getOrder() != null) {
63-
healthAggregator.setStatusOrder(properties.getOrder());
68+
if (properties.getStatus().getOrder() != null) {
69+
healthAggregator.setStatusOrder(properties.getStatus().getOrder());
6470
}
6571
return healthAggregator;
6672
}
6773

6874
@Bean
6975
@ConditionalOnMissingBean(HealthIndicatorRegistry.class)
70-
public HealthIndicatorRegistry healthIndicatorRegistry(ApplicationContext applicationContext) {
71-
return HealthIndicatorRegistryBeans.get(applicationContext);
76+
public HealthIndicatorRegistry healthIndicatorRegistry(HealthIndicatorProperties properties,
77+
HealthAggregator healthAggregator, ApplicationContext applicationContext) {
78+
HealthIndicatorRegistry registry = HealthIndicatorRegistryBeans.get(applicationContext);
79+
extractGroups(properties, registry::get).forEach((groupName, groupHealthIndicators) -> registry
80+
.register(groupName, new GroupHealthIndicator(healthAggregator, registry, groupHealthIndicators)));
81+
return registry;
82+
}
83+
84+
private static <T> Map<String, Set<String>> extractGroups(HealthIndicatorProperties properties,
85+
Function<String, T> healthIndicatorByName) {
86+
Map<String, Set<String>> groupDefinitions = new LinkedHashMap<>();
87+
properties.getGroups().forEach((groupName, indicatorNames) -> {
88+
if (healthIndicatorByName.apply(groupName) != null) {
89+
throw new IllegalArgumentException("Could not register health indicator group named '" + groupName
90+
+ "', an health indicator with that name is already registered");
91+
}
92+
Set<String> groupHealthIndicators = new LinkedHashSet<>();
93+
indicatorNames.forEach((name) -> {
94+
T healthIndicator = healthIndicatorByName.apply(name);
95+
if (healthIndicator != null) {
96+
groupHealthIndicators.add(name);
97+
}
98+
});
99+
groupDefinitions.put(groupName, groupHealthIndicators);
100+
});
101+
return groupDefinitions;
72102
}
73103

74104
@Configuration(proxyBeanMethods = false)
@@ -77,11 +107,15 @@ static class ReactiveHealthIndicatorConfiguration {
77107

78108
@Bean
79109
@ConditionalOnMissingBean
80-
public ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry(
81-
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
110+
public ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry(HealthIndicatorProperties properties,
111+
HealthAggregator healthAggregator, Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
82112
Map<String, HealthIndicator> healthIndicators) {
83-
return new ReactiveHealthIndicatorRegistryFactory()
113+
ReactiveHealthIndicatorRegistry registry = new ReactiveHealthIndicatorRegistryFactory()
84114
.createReactiveHealthIndicatorRegistry(reactiveHealthIndicators, healthIndicators);
115+
extractGroups(properties, registry::get)
116+
.forEach((groupName, groupHealthIndicators) -> registry.register(groupName,
117+
new GroupReactiveHealthIndicator(healthAggregator, registry, groupHealthIndicators)));
118+
return registry;
85119
}
86120

87121
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,32 +28,52 @@
2828
* @author Christian Dupuis
2929
* @since 2.0.0
3030
*/
31-
@ConfigurationProperties(prefix = "management.health.status")
31+
@ConfigurationProperties(prefix = "management.health")
3232
public class HealthIndicatorProperties {
3333

3434
/**
35-
* Comma-separated list of health statuses in order of severity.
35+
* Health indicator groups. Each entry maps the name of a group with a list of health
36+
* indicators to associate with the group.
3637
*/
37-
private List<String> order = null;
38+
private final Map<String, List<String>> groups = new HashMap<>();
3839

39-
/**
40-
* Mapping of health statuses to HTTP status codes. By default, registered health
41-
* statuses map to sensible defaults (for example, UP maps to 200).
42-
*/
43-
private final Map<String, Integer> httpMapping = new HashMap<>();
40+
private final Status status = new Status();
4441

45-
public List<String> getOrder() {
46-
return this.order;
42+
public Map<String, List<String>> getGroups() {
43+
return this.groups;
4744
}
4845

49-
public void setOrder(List<String> statusOrder) {
50-
if (statusOrder != null && !statusOrder.isEmpty()) {
51-
this.order = statusOrder;
52-
}
46+
public Status getStatus() {
47+
return this.status;
5348
}
5449

55-
public Map<String, Integer> getHttpMapping() {
56-
return this.httpMapping;
50+
public static class Status {
51+
52+
/**
53+
* Comma-separated list of health statuses in order of severity.
54+
*/
55+
private List<String> order = null;
56+
57+
/**
58+
* Mapping of health statuses to HTTP status codes. By default, registered health
59+
* statuses map to sensible defaults (for example, UP maps to 200).
60+
*/
61+
private final Map<String, Integer> httpMapping = new HashMap<>();
62+
63+
public List<String> getOrder() {
64+
return this.order;
65+
}
66+
67+
public void setOrder(List<String> statusOrder) {
68+
if (statusOrder != null && !statusOrder.isEmpty()) {
69+
this.order = statusOrder;
70+
}
71+
}
72+
73+
public Map<String, Integer> getHttpMapping() {
74+
return this.httpMapping;
75+
}
76+
5777
}
5878

5979
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.health;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
/**
23+
* Aggregates the overall health of a system by caching {@link Health} per indicator.
24+
*
25+
* @author Stephane Nicoll
26+
* @since 2.2.0
27+
*/
28+
public class AggregatedHealth {
29+
30+
private static final Health UNKNOWN_HEALTH = Health.unknown().build();
31+
32+
private final HealthIndicatorRegistry registry;
33+
34+
private final Map<String, Health> healths;
35+
36+
/**
37+
* Create an instance based on the specified {@link HealthIndicatorRegistry}.
38+
* @param registry the registry to use to retrieve an indicator by name
39+
*/
40+
public AggregatedHealth(HealthIndicatorRegistry registry) {
41+
this.registry = registry;
42+
this.healths = new HashMap<>();
43+
}
44+
45+
/**
46+
* Return the {@link Health} of the indicator with the specified {@code name} for this
47+
* instance or {@code null} if no such indicator exists. When calling this method
48+
* several times for a given indicator, the same {@link Health} instance is returned.
49+
* @param name the name of a {@link HealthIndicator}
50+
* @return the {@link Health} of the indicator with the specified name
51+
*/
52+
public Health health(String name) {
53+
Health health = this.healths.computeIfAbsent(name, this::determineHealth);
54+
return (health != UNKNOWN_HEALTH) ? health : null;
55+
}
56+
57+
private Health determineHealth(String name) {
58+
HealthIndicator healthIndicator = this.registry.get(name);
59+
if (healthIndicator == null) {
60+
return UNKNOWN_HEALTH;
61+
}
62+
if (healthIndicator instanceof AggregatedHealthIndicator) {
63+
return ((AggregatedHealthIndicator) healthIndicator).health(this);
64+
}
65+
return healthIndicator.health();
66+
}
67+
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.health;
18+
19+
/**
20+
* An extended {@link HealthIndicator} that computes a global {@link Health} based on a
21+
* {@link AggregatedHealth}.
22+
*
23+
* @author Stephane Nicoll
24+
* @since 2.2.0
25+
*/
26+
@FunctionalInterface
27+
public interface AggregatedHealthIndicator extends HealthIndicator {
28+
29+
@Override
30+
default Health health() {
31+
return health(new AggregatedHealth(new DefaultHealthIndicatorRegistry()));
32+
}
33+
34+
/**
35+
* Return an indication of health based on the specified {@link AggregatedHealth}.
36+
* @param aggregatedHealth the already computed health
37+
* @return the health that this aggregate represents
38+
*/
39+
Health health(AggregatedHealth aggregatedHealth);
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.health;
18+
19+
import reactor.core.publisher.Mono;
20+
21+
/**
22+
* An extended {@link ReactiveHealthIndicator} that computes a global {@link Health} based
23+
* on a {@link ReactiveAggregatedHealth}.
24+
*
25+
* @author Stephane Nicoll
26+
* @since 2.2.0
27+
*/
28+
@FunctionalInterface
29+
public interface AggregatedReactiveHealthIndicator extends ReactiveHealthIndicator {
30+
31+
@Override
32+
default Mono<Health> health() {
33+
return health(new ReactiveAggregatedHealth(new DefaultReactiveHealthIndicatorRegistry()));
34+
}
35+
36+
/**
37+
* Return an indication of health based on the specified
38+
* {@link ReactiveAggregatedHealth}.
39+
* @param aggregatedHealth the already computed health
40+
* @return a {@link Mono} to the health that this aggregate represents
41+
*/
42+
Mono<Health> health(ReactiveAggregatedHealth aggregatedHealth);
43+
44+
}

0 commit comments

Comments
 (0)