Validate health group includes and excludes
Closes gh-34360
This commit is contained in:
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…
Reference in New Issue