Allow part of a composite contributor in a health group

Closes gh-23027

Co-authored-by: Phillip Webb <pwebb@vmware.com>
This commit is contained in:
Madhura Bhave
2021-08-17 14:10:35 -07:00
parent fd2fbcb3c6
commit 8fd9eb72d4
10 changed files with 228 additions and 26 deletions

View File

@@ -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;
}
@SuppressWarnings("unchecked")
private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor,
boolean showComponents, boolean showDetails, Set<String> groupNames, boolean isNested) {
if (contributor instanceof NamedContributors) {
return getAggregateHealth(apiVersion, group, (NamedContributors<C>) contributor, showComponents,
showDetails, groupNames, isNested);
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 (contributor != null) ? getHealth((C) contributor, showDetails) : null;
return name.toString();
}
private T getAggregateHealth(ApiVersion apiVersion, HealthEndpointGroup group,
NamedContributors<C> namedContributors, boolean showComponents, boolean showDetails, Set<String> groupNames,
boolean isNested) {
@SuppressWarnings("unchecked")
private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, String name, Object contributor,
boolean showComponents, boolean showDetails, Set<String> groupNames) {
if (contributor instanceof NamedContributors) {
return getAggregateContribution(apiVersion, group, name, (NamedContributors<C>) contributor, showComponents,
showDetails, groupNames);
}
if (contributor != null && (name.isEmpty() || group.isMember(name))) {
return getHealth((C) contributor, showDetails);
}
return null;
}
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()) {

View File

@@ -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();

View File

@@ -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));

View File

@@ -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();