|
20 | 20 | import java.util.Iterator;
|
21 | 21 | import java.util.LinkedHashMap;
|
22 | 22 | import java.util.Map;
|
| 23 | +import java.util.Set; |
23 | 24 |
|
24 | 25 | import org.springframework.beans.BeansException;
|
25 | 26 | import org.springframework.beans.factory.ObjectProvider;
|
| 27 | +import org.springframework.beans.factory.SmartInitializingSingleton; |
26 | 28 | import org.springframework.beans.factory.config.BeanPostProcessor;
|
27 | 29 | import org.springframework.boot.actuate.health.CompositeHealthContributor;
|
28 | 30 | import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
|
|
35 | 37 | import org.springframework.boot.actuate.health.HealthIndicator;
|
36 | 38 | import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
|
37 | 39 | import org.springframework.boot.actuate.health.NamedContributor;
|
| 40 | +import org.springframework.boot.actuate.health.NamedContributors; |
38 | 41 | import org.springframework.boot.actuate.health.ReactiveHealthContributor;
|
39 | 42 | import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
|
40 | 43 | import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
|
41 | 44 | import org.springframework.boot.actuate.health.SimpleStatusAggregator;
|
42 | 45 | import org.springframework.boot.actuate.health.StatusAggregator;
|
43 | 46 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
| 47 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
44 | 48 | import org.springframework.context.ApplicationContext;
|
45 | 49 | import org.springframework.context.annotation.Bean;
|
46 | 50 | import org.springframework.context.annotation.Configuration;
|
47 | 51 | import org.springframework.util.ClassUtils;
|
| 52 | +import org.springframework.util.CollectionUtils; |
48 | 53 |
|
49 | 54 | /**
|
50 | 55 | * Configuration for {@link HealthEndpoint} infrastructure beans.
|
@@ -85,6 +90,14 @@ HealthContributorRegistry healthContributorRegistry(ApplicationContext applicati
|
85 | 90 | return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames());
|
86 | 91 | }
|
87 | 92 |
|
| 93 | + @Bean |
| 94 | + @ConditionalOnProperty(name = "management.endpoint.health.validate-group-membership", havingValue = "true", |
| 95 | + matchIfMissing = true) |
| 96 | + HealthEndpointGroupMembershipValidator healthEndpointGroupMembershipValidator(HealthEndpointProperties properties, |
| 97 | + HealthContributorRegistry healthContributorRegistry) { |
| 98 | + return new HealthEndpointGroupMembershipValidator(properties, healthContributorRegistry); |
| 99 | + } |
| 100 | + |
88 | 101 | @Bean
|
89 | 102 | @ConditionalOnMissingBean
|
90 | 103 | HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups,
|
@@ -204,4 +217,75 @@ Map<String, HealthContributor> get() {
|
204 | 217 |
|
205 | 218 | }
|
206 | 219 |
|
| 220 | + /** |
| 221 | + * {@link SmartInitializingSingleton} that validates health endpoint group membership, |
| 222 | + * throwing a {@link NoSuchHealthContributorException} if an included or excluded |
| 223 | + * contributor does not exist. |
| 224 | + */ |
| 225 | + static class HealthEndpointGroupMembershipValidator implements SmartInitializingSingleton { |
| 226 | + |
| 227 | + private final HealthEndpointProperties properties; |
| 228 | + |
| 229 | + private final HealthContributorRegistry registry; |
| 230 | + |
| 231 | + HealthEndpointGroupMembershipValidator(HealthEndpointProperties properties, |
| 232 | + HealthContributorRegistry registry) { |
| 233 | + this.properties = properties; |
| 234 | + this.registry = registry; |
| 235 | + } |
| 236 | + |
| 237 | + @Override |
| 238 | + public void afterSingletonsInstantiated() { |
| 239 | + validateGroups(); |
| 240 | + } |
| 241 | + |
| 242 | + private void validateGroups() { |
| 243 | + this.properties.getGroup().forEach((name, group) -> { |
| 244 | + validate(group.getInclude(), "Included", name); |
| 245 | + validate(group.getExclude(), "Excluded", name); |
| 246 | + }); |
| 247 | + } |
| 248 | + |
| 249 | + private void validate(Set<String> names, String type, String group) { |
| 250 | + if (CollectionUtils.isEmpty(names)) { |
| 251 | + return; |
| 252 | + } |
| 253 | + for (String name : names) { |
| 254 | + if ("*".equals(name)) { |
| 255 | + return; |
| 256 | + } |
| 257 | + String[] path = name.split("/"); |
| 258 | + if (!contributorExists(path)) { |
| 259 | + throw new NoSuchHealthContributorException(type, name, group); |
| 260 | + } |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + private boolean contributorExists(String[] path) { |
| 265 | + int pathOffset = 0; |
| 266 | + Object contributor = this.registry; |
| 267 | + while (pathOffset < path.length) { |
| 268 | + if (!(contributor instanceof NamedContributors)) { |
| 269 | + return false; |
| 270 | + } |
| 271 | + contributor = ((NamedContributors<?>) contributor).getContributor(path[pathOffset]); |
| 272 | + pathOffset++; |
| 273 | + } |
| 274 | + return (contributor != null); |
| 275 | + } |
| 276 | + |
| 277 | + /** |
| 278 | + * Thrown when a contributor that does not exist is included in or excluded from a |
| 279 | + * group. |
| 280 | + */ |
| 281 | + static class NoSuchHealthContributorException extends RuntimeException { |
| 282 | + |
| 283 | + NoSuchHealthContributorException(String type, String name, String group) { |
| 284 | + super(type + " health contributor '" + name + "' in group '" + group + "' does not exist"); |
| 285 | + } |
| 286 | + |
| 287 | + } |
| 288 | + |
| 289 | + } |
| 290 | + |
207 | 291 | }
|
0 commit comments