Allow part of a composite contributor in a health group

Closes gh-23027

Co-authored-by: Phillip Webb <pwebb@vmware.com>
pull/27768/head
Madhura Bhave 3 years ago
parent fd2fbcb3c6
commit 8fd9eb72d4

@ -26,6 +26,7 @@ import java.util.stream.Collectors;
* Member predicate that matches based on {@code include} and {@code exclude} sets.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class IncludeExcludeGroupMemberPredicate implements Predicate<String> {
@ -40,15 +41,30 @@ class IncludeExcludeGroupMemberPredicate implements Predicate<String> {
@Override
public boolean test(String name) {
return testCleanName(clean(name));
}
private boolean testCleanName(String name) {
return isIncluded(name) && !isExcluded(name);
}
private boolean isIncluded(String name) {
return this.include.isEmpty() || this.include.contains("*") || this.include.contains(clean(name));
return this.include.isEmpty() || this.include.contains("*") || isIncludedName(name);
}
private boolean isIncludedName(String name) {
if (this.include.contains(name)) {
return true;
}
if (name.contains("/")) {
String parent = name.substring(0, name.lastIndexOf("/"));
return isIncludedName(parent);
}
return false;
}
private boolean isExcluded(String name) {
return this.exclude.contains("*") || this.exclude.contains(clean(name));
return this.exclude.contains("*") || this.exclude.contains(name);
}
private Set<String> clean(Set<String> names) {
@ -60,7 +76,7 @@ class IncludeExcludeGroupMemberPredicate implements Predicate<String> {
}
private String clean(String name) {
return name.trim();
return (name != null) ? name.trim() : null;
}
}

@ -94,6 +94,24 @@ class IncludeExcludeGroupMemberPredicateTests {
assertThat(predicate).accepts("myEndpoint").rejects("d");
}
@Test
void testWhenSpecifiedIncludeWithSlash() {
Predicate<String> predicate = include("test/a").exclude();
assertThat(predicate).accepts("test/a").rejects("test").rejects("test/b");
}
@Test
void specifiedIncludeShouldIncludeNested() {
Predicate<String> predicate = include("test").exclude();
assertThat(predicate).accepts("test/a/d").accepts("test/b").rejects("foo");
}
@Test
void specifiedIncludeShouldNotIncludeExcludedNested() {
Predicate<String> predicate = include("test").exclude("test/b");
assertThat(predicate).accepts("test/a").rejects("test/b").rejects("foo");
}
private Builder include(String... include) {
return new Builder(include);
}

@ -25,6 +25,7 @@ import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Base class for health endpoints and health endpoint extensions.
@ -86,8 +87,12 @@ abstract class HealthEndpointSupport<C, T> {
return null;
}
Object contributor = getContributor(path, pathOffset);
if (contributor == null) {
return null;
}
String name = getName(path, pathOffset);
Set<String> groupNames = isSystemHealth ? this.groups.getNames() : null;
T health = getContribution(apiVersion, group, contributor, showComponents, showDetails, groupNames, false);
T health = getContribution(apiVersion, group, name, contributor, showComponents, showDetails, groupNames);
return (health != null) ? new HealthResult<>(health, group) : null;
}
@ -104,29 +109,39 @@ abstract class HealthEndpointSupport<C, T> {
return contributor;
}
private String getName(String[] path, int pathOffset) {
StringBuilder name = new StringBuilder();
while (pathOffset < path.length) {
name.append((name.length() != 0) ? "/" : "");
name.append(path[pathOffset]);
pathOffset++;
}
return name.toString();
}
@SuppressWarnings("unchecked")
private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor,
boolean showComponents, boolean showDetails, Set<String> groupNames, boolean isNested) {
private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, String name, Object contributor,
boolean showComponents, boolean showDetails, Set<String> groupNames) {
if (contributor instanceof NamedContributors) {
return getAggregateHealth(apiVersion, group, (NamedContributors<C>) contributor, showComponents,
showDetails, groupNames, isNested);
return getAggregateContribution(apiVersion, group, name, (NamedContributors<C>) contributor, showComponents,
showDetails, groupNames);
}
return (contributor != null) ? getHealth((C) contributor, showDetails) : null;
if (contributor != null && (name.isEmpty() || group.isMember(name))) {
return getHealth((C) contributor, showDetails);
}
return null;
}
private T getAggregateHealth(ApiVersion apiVersion, HealthEndpointGroup group,
NamedContributors<C> namedContributors, boolean showComponents, boolean showDetails, Set<String> groupNames,
boolean isNested) {
private T getAggregateContribution(ApiVersion apiVersion, HealthEndpointGroup group, String name,
NamedContributors<C> namedContributors, boolean showComponents, boolean showDetails,
Set<String> groupNames) {
String prefix = (StringUtils.hasText(name)) ? name + "/" : "";
Map<String, T> contributions = new LinkedHashMap<>();
for (NamedContributor<C> namedContributor : namedContributors) {
String name = namedContributor.getName();
C contributor = namedContributor.getContributor();
if (group.isMember(name) || isNested) {
T contribution = getContribution(apiVersion, group, contributor, showComponents, showDetails, null,
true);
if (contribution != null) {
contributions.put(name, contribution);
}
for (NamedContributor<C> child : namedContributors) {
T contribution = getContribution(apiVersion, group, prefix + child.getName(), child.getContributor(),
showComponents, showDetails, null);
if (contribution != null) {
contributions.put(child.getName(), contribution);
}
}
if (contributions.isEmpty()) {

@ -43,13 +43,19 @@ abstract class NamedContributorsMapAdapter<V, C> implements NamedContributors<C>
NamedContributorsMapAdapter(Map<String, V> map, Function<V, ? extends C> valueAdapter) {
Assert.notNull(map, "Map must not be null");
Assert.notNull(valueAdapter, "ValueAdapter must not be null");
map.keySet().forEach((key) -> Assert.notNull(key, "Map must not contain null keys"));
map.keySet().forEach(this::validateKey);
map.values().stream().map(valueAdapter)
.forEach((value) -> Assert.notNull(value, "Map must not contain null values"));
this.map = Collections.unmodifiableMap(new LinkedHashMap<>(map));
this.valueAdapter = valueAdapter;
}
private void validateKey(String value) {
Assert.notNull(value, "Map must not contain null keys");
Assert.isTrue(!value.contains("/"), "Map keys must not contain a '/'");
}
@Override
public Iterator<NamedContributor<C>> iterator() {
Iterator<Entry<String, V>> iterator = this.map.entrySet().iterator();

@ -19,6 +19,7 @@ package org.springframework.boot.actuate.health;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
@ -224,6 +225,92 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
assertThat(health.getComponents()).containsKey("test");
}
@Test
void getHealthWhenGroupContainsComponentOfCompositeContributorReturnsHealth() {
CompositeHealth health = getCompositeHealth((name) -> name.equals("test/spring-1"));
assertThat(health.getComponents()).containsKey("test");
CompositeHealth test = (CompositeHealth) health.getComponents().get("test");
assertThat(test.getComponents()).containsKey("spring-1");
assertThat(test.getComponents()).doesNotContainKey("spring-2");
assertThat(test.getComponents()).doesNotContainKey("test");
}
@Test
void getHealthWhenGroupExcludesComponentOfCompositeContributorReturnsHealth() {
CompositeHealth health = getCompositeHealth(
(name) -> name.startsWith("test/") && !name.equals("test/spring-2"));
assertThat(health.getComponents()).containsKey("test");
CompositeHealth test = (CompositeHealth) health.getComponents().get("test");
assertThat(test.getComponents()).containsKey("spring-1");
assertThat(test.getComponents()).doesNotContainKey("spring-2");
}
@Test
void getHealthForPathWhenGroupContainsComponentOfCompositeContributorReturnsHealth() {
Map<String, C> contributors = new LinkedHashMap<>();
contributors.put("spring-1", createNestedHealthContributor("spring-1"));
contributors.put("spring-2", createNestedHealthContributor("spring-2"));
C compositeContributor = createCompositeContributor(contributors);
this.registry.registerContributor("test", compositeContributor);
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup(
(name) -> name.startsWith("test") && !name.equals("test/spring-1/b"));
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "testGroup", "test");
CompositeHealth health = (CompositeHealth) getHealth(result);
assertThat(health.getComponents()).containsKey("spring-1");
assertThat(health.getComponents()).containsKey("spring-2");
CompositeHealth spring1 = (CompositeHealth) health.getComponents().get("spring-1");
CompositeHealth spring2 = (CompositeHealth) health.getComponents().get("spring-2");
assertThat(spring1.getComponents()).containsKey("a");
assertThat(spring1.getComponents()).containsKey("c");
assertThat(spring1.getComponents()).doesNotContainKey("b");
assertThat(spring2.getComponents()).containsKey("a");
assertThat(spring2.getComponents()).containsKey("c");
assertThat(spring2.getComponents()).containsKey("b");
}
@Test
void getHealthForComponentPathWhenNotPartOfGroup() {
Map<String, C> contributors = new LinkedHashMap<>();
contributors.put("spring-1", createNestedHealthContributor("spring-1"));
contributors.put("spring-2", createNestedHealthContributor("spring-2"));
C compositeContributor = createCompositeContributor(contributors);
this.registry.registerContributor("test", compositeContributor);
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup(
(name) -> name.startsWith("test") && !name.equals("test/spring-1/b"));
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "testGroup", "test", "spring-1", "b");
assertThat(result).isNull();
}
private CompositeHealth getCompositeHealth(Predicate<String> memberPredicate) {
C contributor1 = createContributor(this.up);
C contributor2 = createContributor(this.down);
Map<String, C> contributors = new LinkedHashMap<>();
contributors.put("spring-1", contributor1);
contributors.put("spring-2", contributor2);
C compositeContributor = createCompositeContributor(contributors);
this.registry.registerContributor("test", compositeContributor);
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup(memberPredicate);
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "testGroup");
return (CompositeHealth) getHealth(result);
}
private C createNestedHealthContributor(String name) {
Map<String, C> map = new LinkedHashMap<>();
map.put("a", createContributor(Health.up().withDetail("hello", name + "-a").build()));
map.put("b", createContributor(Health.up().withDetail("hello", name + "-b").build()));
map.put("c", createContributor(Health.up().withDetail("hello", name + "-c").build()));
return createCompositeContributor(map);
}
@Test
void getHealthWhenGroupHasAdditionalPath() {
this.registry.registerContributor("test", createContributor(this.up));

@ -64,6 +64,14 @@ class NamedContributorsMapAdapterTests {
.withMessage("Map must not contain null keys");
}
@Test
void createWhenMapContainsKeyWithSlashThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new TestNamedContributorsMapAdapter<>(Collections.singletonMap("test/key", "test"),
Function.identity()))
.withMessage("Map keys must not contain a '/'");
}
@Test
void iterateReturnsAdaptedEntries() {
TestNamedContributorsMapAdapter<String> adapter = createAdapter();

@ -934,6 +934,20 @@ It's also possible to override the `show-details` and `roles` properties if requ
TIP: You can use `@Qualifier("groupname")` if you need to register custom `StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group.
A health group can also include/exclude a `CompositeHealthContributor`.
You can also include/exclude only a certain component of a `CompositeHealthContributor`.
This can be done using the fully qualified name of the component as follows:
[source,properties,indent=0,subs="verbatim"]
----
management.endpoint.health.group.custom.include="test/primary"
management.endpoint.health.group.custom.exclude="test/primary/b"
----
In the example above, the `custom` group will include the `HealthContributor` with the name `primary` which is a component of the composite `test`.
Here, `primary` itself is a composite and the `HealthContributor` with the name `b` will be excluded from the `custom` group.
Health groups can be made available at an additional path on either the main or management port.
This is useful in cloud environments such as Kubernetes, where it is quite common to use a separate management port for the actuator endpoints for security purposes.
Having a separate port could lead to unreliable health checks because the main application might not work properly even if the health check is successful.

@ -16,8 +16,13 @@
package smoketest.actuator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@ -33,7 +38,27 @@ public class SampleActuatorApplication {
@Bean
public HealthIndicator helloHealthIndicator() {
return () -> Health.up().withDetail("hello", "world").build();
return createHealthIndicator("world");
}
@Bean
public HealthContributor compositeHelloHealthContributor() {
Map<String, HealthContributor> map = new LinkedHashMap<>();
map.put("spring", createNestedHealthContributor("spring"));
map.put("boot", createNestedHealthContributor("boot"));
return CompositeHealthContributor.fromMap(map);
}
private HealthContributor createNestedHealthContributor(String name) {
Map<String, HealthContributor> map = new LinkedHashMap<>();
map.put("a", createHealthIndicator(name + "-a"));
map.put("b", createHealthIndicator(name + "-b"));
map.put("c", createHealthIndicator(name + "-c"));
return CompositeHealthContributor.fromMap(map);
}
private HealthIndicator createHealthIndicator(String value) {
return () -> Health.up().withDetail("hello", value).build();
}
}

@ -23,4 +23,8 @@ management.endpoint.health.show-details=always
management.endpoint.health.group.ready.include=db,diskSpace
management.endpoint.health.group.live.include=example,hello,db
management.endpoint.health.group.live.show-details=never
management.endpoint.health.group.comp.include=compositeHello/spring/a,compositeHello/spring/c
management.endpoint.health.group.comp.show-details=always
management.endpoints.migrate-legacy-ids=true

@ -67,7 +67,16 @@ abstract class AbstractManagementPortAndPathSampleActuatorApplicationTests {
ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password")
.getForEntity("http://localhost:" + this.managementPort + "/admin/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\",\"groups\":[\"live\",\"ready\"]}");
assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\",\"groups\":[\"comp\",\"live\",\"ready\"]}");
}
@Test
void testGroupWithComposite() {
ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password")
.getForEntity("http://localhost:" + this.managementPort + "/admin/health/comp", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains(
"components\":{\"a\":{\"status\":\"UP\",\"details\":{\"hello\":\"spring-a\"}},\"c\":{\"status\":\"UP\",\"details\":{\"hello\":\"spring-c\"}}");
}
@Test

Loading…
Cancel
Save