Validate health group includes and excludes

Closes gh-34360
pull/35090/head
Andy Wilkinson 2 years ago
parent a03fe8befc
commit c55d398f95

@ -20,9 +20,11 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
@ -35,16 +37,19 @@ import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.NamedContributor;
import org.springframework.boot.actuate.health.NamedContributors;
import org.springframework.boot.actuate.health.ReactiveHealthContributor;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
import org.springframework.boot.actuate.health.SimpleStatusAggregator;
import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/**
* Configuration for {@link HealthEndpoint} infrastructure beans.
@ -85,6 +90,14 @@ class HealthEndpointConfiguration {
return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames());
}
@Bean
@ConditionalOnProperty(name = "management.endpoint.health.validate-group-membership", havingValue = "true",
matchIfMissing = true)
HealthEndpointGroupMembershipValidator healthEndpointGroupMembershipValidator(HealthEndpointProperties properties,
HealthContributorRegistry healthContributorRegistry) {
return new HealthEndpointGroupMembershipValidator(properties, healthContributorRegistry);
}
@Bean
@ConditionalOnMissingBean
HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups,
@ -204,4 +217,75 @@ class HealthEndpointConfiguration {
}
/**
* {@link SmartInitializingSingleton} that validates health endpoint group membership,
* throwing a {@link NoSuchHealthContributorException} if an included or excluded
* contributor does not exist.
*/
static class HealthEndpointGroupMembershipValidator implements SmartInitializingSingleton {
private final HealthEndpointProperties properties;
private final HealthContributorRegistry registry;
HealthEndpointGroupMembershipValidator(HealthEndpointProperties properties,
HealthContributorRegistry registry) {
this.properties = properties;
this.registry = registry;
}
@Override
public void afterSingletonsInstantiated() {
validateGroups();
}
private void validateGroups() {
this.properties.getGroup().forEach((name, group) -> {
validate(group.getInclude(), "Included", name);
validate(group.getExclude(), "Excluded", name);
});
}
private void validate(Set<String> names, String type, String group) {
if (CollectionUtils.isEmpty(names)) {
return;
}
for (String name : names) {
if ("*".equals(name)) {
return;
}
String[] path = name.split("/");
if (!contributorExists(path)) {
throw new NoSuchHealthContributorException(type, name, group);
}
}
}
private boolean contributorExists(String[] path) {
int pathOffset = 0;
Object contributor = this.registry;
while (pathOffset < path.length) {
if (!(contributor instanceof NamedContributors)) {
return false;
}
contributor = ((NamedContributors<?>) contributor).getContributor(path[pathOffset]);
pathOffset++;
}
return (contributor != null);
}
/**
* Thrown when a contributor that does not exist is included in or excluded from a
* group.
*/
static class NoSuchHealthContributorException extends RuntimeException {
NoSuchHealthContributorException(String type, String name, String group) {
super(type + " health contributor '" + name + "' in group '" + group + "' does not exist");
}
}
}
}

@ -55,6 +55,12 @@
"UNKNOWN"
]
},
{
"name": "management.endpoint.health.validate-group-membership",
"type": "java.lang.Boolean",
"description": "Whether to validate health group membership on startup. Validation fails if a group includes or excludes a health contributor that does not exist.",
"defaultValue": true
},
{
"name": "management.endpoints.enabled-by-default",
"type": "java.lang.Boolean",

@ -25,10 +25,12 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration.HealthEndpointGroupMembershipValidator.NoSuchHealthContributorException;
import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry;
import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry;
import org.springframework.boot.actuate.health.Health;
@ -141,6 +143,41 @@ class HealthEndpointAutoConfigurationTests {
});
}
@Test
void runFailsWhenHealthEndpointGroupIncludesContributorThatDoesNotExist() {
this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class)
.withPropertyValues("management.endpoint.health.group.ready.include=composite/b/c,nope")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).isInstanceOf(NoSuchHealthContributorException.class)
.hasMessage("Included health contributor 'nope' in group 'ready' does not exist");
});
}
@Test
void runFailsWhenHealthEndpointGroupExcludesContributorThatDoesNotExist() {
this.contextRunner
.withPropertyValues("management.endpoint.health.group.ready.exclude=composite/b/d",
"management.endpoint.health.group.ready.include=*")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).isInstanceOf(NoSuchHealthContributorException.class)
.hasMessage("Excluded health contributor 'composite/b/d' in group 'ready' does not exist");
});
}
@Test
void runCreatesHealthEndpointGroupThatIncludesContributorThatDoesNotExistWhenValidationIsDisabled() {
this.contextRunner
.withPropertyValues("management.endpoint.health.validate-group-membership=false",
"management.endpoint.health.group.ready.include=nope")
.run((context) -> {
HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class);
assertThat(groups).isInstanceOf(AutoConfiguredHealthEndpointGroups.class);
assertThat(groups.getNames()).containsOnly("ready");
});
}
@Test
void runWhenHasHealthEndpointGroupsBeanDoesNotCreateAdditionalHealthEndpointGroups() {
this.contextRunner.withUserConfiguration(HealthEndpointGroupsConfiguration.class)
@ -320,6 +357,17 @@ class HealthEndpointAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CompositeHealthIndicatorConfiguration {
@Bean
CompositeHealthContributor compositeHealthIndicator() {
return CompositeHealthContributor.fromMap(Map.of("a", (HealthIndicator) () -> Health.up().build(), "b",
CompositeHealthContributor.fromMap(Map.of("c", (HealthIndicator) () -> Health.up().build()))));
}
}
@Configuration(proxyBeanMethods = false)
static class StatusAggregatorConfiguration {

@ -789,6 +789,9 @@ Similarly, to create a group that excludes the database indicators from the grou
exclude: "db"
----
By default, startup will fail if a health group includes or excludes a health indicator that does not exist.
To disable this behavior set configprop:management.endpoint.health.validate-group-membership[] to `false`.
By default, groups inherit the same `StatusAggregator` and `HttpCodeStatusMapper` settings as the system health.
However, you can also define these on a per-group basis.
You can also override the `show-details` and `roles` properties if required:

Loading…
Cancel
Save