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.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.springframework.beans.BeansException;
|
import org.springframework.beans.BeansException;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
import org.springframework.boot.actuate.health.CompositeHealthContributor;
|
import org.springframework.boot.actuate.health.CompositeHealthContributor;
|
||||||
import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
|
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.HealthIndicator;
|
||||||
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
|
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
|
||||||
import org.springframework.boot.actuate.health.NamedContributor;
|
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.ReactiveHealthContributor;
|
||||||
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
|
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
|
||||||
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
|
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
|
||||||
import org.springframework.boot.actuate.health.SimpleStatusAggregator;
|
import org.springframework.boot.actuate.health.SimpleStatusAggregator;
|
||||||
import org.springframework.boot.actuate.health.StatusAggregator;
|
import org.springframework.boot.actuate.health.StatusAggregator;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for {@link HealthEndpoint} infrastructure beans.
|
* Configuration for {@link HealthEndpoint} infrastructure beans.
|
||||||
|
@ -85,6 +90,14 @@ class HealthEndpointConfiguration {
|
||||||
return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames());
|
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
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups,
|
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"
|
"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",
|
"name": "management.endpoints.enabled-by-default",
|
||||||
"type": "java.lang.Boolean",
|
"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.EndpointAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
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.ApiVersion;
|
||||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
|
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.DefaultHealthContributorRegistry;
|
||||||
import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry;
|
import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry;
|
||||||
import org.springframework.boot.actuate.health.Health;
|
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
|
@Test
|
||||||
void runWhenHasHealthEndpointGroupsBeanDoesNotCreateAdditionalHealthEndpointGroups() {
|
void runWhenHasHealthEndpointGroupsBeanDoesNotCreateAdditionalHealthEndpointGroups() {
|
||||||
this.contextRunner.withUserConfiguration(HealthEndpointGroupsConfiguration.class)
|
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)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class StatusAggregatorConfiguration {
|
static class StatusAggregatorConfiguration {
|
||||||
|
|
||||||
|
|
|
@ -789,6 +789,9 @@ Similarly, to create a group that excludes the database indicators from the grou
|
||||||
exclude: "db"
|
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.
|
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.
|
However, you can also define these on a per-group basis.
|
||||||
You can also override the `show-details` and `roles` properties if required:
|
You can also override the `show-details` and `roles` properties if required:
|
||||||
|
|
Loading…
Reference in New Issue