Add health endpoint 'show-components' support

Add a `show-components` property under `management.endpoint.health` and
`management.endpoint.health.group.<name>` that can be used to change
when components are displayed.

Prior to this commit it was only possible to set `show-details` which
offered an "all or nothing" approach to the resulting JSON. The new
switch allows component information to be displayed whilst still hiding
potentially sensitive details returned from the actual `HealthIndicator`.

Closes gh-15076
pull/18371/head
Phillip Webb 5 years ago
parent 69c561a69a
commit a94ab673a3

@ -24,6 +24,9 @@ The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}health/response-fields.adoc[]
NOTE: The response fields above are for the V3 API.
If you need to return V2 JSON you should use an accept header or `application/vnd.spring-boot.actuator.v2+json`
[[health-retrieving-component]]

@ -19,7 +19,7 @@ package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collection;
import java.util.function.Predicate;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
@ -40,7 +40,9 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
private final HttpCodeStatusMapper httpCodeStatusMapper;
private final ShowDetails showDetails;
private final Show showComponents;
private final Show showDetails;
private final Collection<String> roles;
@ -49,14 +51,17 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
* @param members a predicate used to test for group membership
* @param statusAggregator the status aggregator to use
* @param httpCodeStatusMapper the HTTP code status mapper to use
* @param showComponents the show components setting
* @param showDetails the show details setting
* @param roles the roles to match
*/
AutoConfiguredHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
HttpCodeStatusMapper httpCodeStatusMapper, ShowDetails showDetails, Collection<String> roles) {
HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails,
Collection<String> roles) {
this.members = members;
this.statusAggregator = statusAggregator;
this.httpCodeStatusMapper = httpCodeStatusMapper;
this.showComponents = showComponents;
this.showDetails = showDetails;
this.roles = roles;
}
@ -67,9 +72,20 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
}
@Override
public boolean includeDetails(SecurityContext securityContext) {
ShowDetails showDetails = this.showDetails;
switch (showDetails) {
public boolean showComponents(SecurityContext securityContext) {
if (this.showComponents == null) {
return showDetails(securityContext);
}
return getShowResult(securityContext, this.showComponents);
}
@Override
public boolean showDetails(SecurityContext securityContext) {
return getShowResult(securityContext, this.showDetails);
}
private boolean getShowResult(SecurityContext securityContext, Show show) {
switch (show) {
case NEVER:
return false;
case ALWAYS:
@ -77,7 +93,7 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
case WHEN_AUTHORIZED:
return isAuthorized(securityContext);
}
throw new IllegalStateException("Unsupported ShowDetails value " + showDetails);
throw new IllegalStateException("Unsupported 'show' value " + show);
}
private boolean isAuthorized(SecurityContext securityContext) {

@ -31,7 +31,7 @@ import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
@ -65,7 +65,8 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
AutoConfiguredHealthEndpointGroups(ApplicationContext applicationContext, HealthEndpointProperties properties) {
ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext)
? ((ConfigurableApplicationContext) applicationContext).getBeanFactory() : applicationContext;
ShowDetails showDetails = properties.getShowDetails();
Show showComponents = properties.getShowComponents();
Show showDetails = properties.getShowDetails();
Set<String> roles = properties.getRoles();
StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class);
if (statusAggregator == null) {
@ -76,18 +77,20 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping());
}
this.primaryGroup = new AutoConfiguredHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper,
showDetails, roles);
showComponents, showDetails, roles);
this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, httpCodeStatusMapper,
showDetails, roles);
showComponents, showDetails, roles);
}
private Map<String, HealthEndpointGroup> createGroups(Map<String, Group> groupProperties, BeanFactory beanFactory,
StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper,
ShowDetails defaultShowDetails, Set<String> defaultRoles) {
Show defaultShowComponents, Show defaultShowDetails, Set<String> defaultRoles) {
Map<String, HealthEndpointGroup> groups = new LinkedHashMap<String, HealthEndpointGroup>();
groupProperties.forEach((groupName, group) -> {
Status status = group.getStatus();
ShowDetails showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails;
Show showComponents = (group.getShowComponents() != null) ? group.getShowComponents()
: defaultShowComponents;
Show showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails;
Set<String> roles = !CollectionUtils.isEmpty(group.getRoles()) ? group.getRoles() : defaultRoles;
StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, groupName, () -> {
if (!CollectionUtils.isEmpty(status.getOrder())) {
@ -104,7 +107,7 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
});
Predicate<String> members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude());
groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper,
showDetails, roles));
showComponents, showDetails, roles));
});
return Collections.unmodifiableMap(groups);
}

@ -38,10 +38,15 @@ public abstract class HealthProperties {
@NestedConfigurationProperty
private final Status status = new Status();
/**
* When to show components. If not specified the 'show-details' setting will be used.
*/
private Show showComponents;
/**
* When to show full health details.
*/
private ShowDetails showDetails = ShowDetails.NEVER;
private Show showDetails = Show.NEVER;
/**
* Roles used to determine whether or not a user is authorized to be shown details.
@ -53,11 +58,19 @@ public abstract class HealthProperties {
return this.status;
}
public ShowDetails getShowDetails() {
public Show getShowComponents() {
return this.showComponents;
}
public void setShowComponents(Show showComponents) {
this.showComponents = showComponents;
}
public Show getShowDetails() {
return this.showDetails;
}
public void setShowDetails(ShowDetails showDetails) {
public void setShowDetails(Show showDetails) {
this.showDetails = showDetails;
}
@ -102,23 +115,23 @@ public abstract class HealthProperties {
}
/**
* Options for showing details in responses from the {@link HealthEndpoint} web
* Options for showing items in responses from the {@link HealthEndpoint} web
* extensions.
*/
public enum ShowDetails {
public enum Show {
/**
* Never show details in the response.
* Never show the item in the response.
*/
NEVER,
/**
* Show details in the response when accessed by an authorized user.
* Show the item in the response when accessed by an authorized user.
*/
WHEN_AUTHORIZED,
/**
* Always show details in the response.
* Always show the item in the response.
*/
ALWAYS

@ -146,7 +146,12 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
}
@Override
public boolean includeDetails(SecurityContext securityContext) {
public boolean showComponents(SecurityContext securityContext) {
return true;
}
@Override
public boolean showDetails(SecurityContext securityContext) {
return true;
}

@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
@ -60,7 +60,7 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void isMemberWhenMemberPredicateMatchesAcceptsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.isMember("albert")).isTrue();
assertThat(group.isMember("arnold")).isTrue();
}
@ -68,72 +68,134 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void isMemberWhenMemberPredicateRejectsReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.isMember("bert")).isFalse();
assertThat(group.isMember("ernie")).isFalse();
}
@Test
void includeDetailsWhenShowDetailsIsNeverReturnsFalse() {
void showDetailsWhenShowDetailsIsNeverReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.NEVER, Collections.emptySet());
assertThat(group.includeDetails(SecurityContext.NONE)).isFalse();
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
assertThat(group.showDetails(SecurityContext.NONE)).isFalse();
}
@Test
void includeDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
void showDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
assertThat(group.includeDetails(SecurityContext.NONE)).isTrue();
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.showDetails(SecurityContext.NONE)).isTrue();
}
@Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(group.includeDetails(this.securityContext)).isFalse();
assertThat(group.showDetails(this.securityContext)).isFalse();
}
@Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.includeDetails(this.securityContext)).isTrue();
assertThat(group.showDetails(this.securityContext)).isTrue();
}
@Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.includeDetails(this.securityContext)).isTrue();
assertThat(group.showDetails(this.securityContext)).isTrue();
}
@Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "rot", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.includeDetails(this.securityContext)).isFalse();
assertThat(group.showDetails(this.securityContext)).isFalse();
}
@Test
void showComponentsWhenShowComponentsIsNullDelegatesToShowDetails() {
AutoConfiguredHealthEndpointGroup alwaysGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(alwaysGroup.showComponents(SecurityContext.NONE)).isTrue();
AutoConfiguredHealthEndpointGroup neverGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
assertThat(neverGroup.showComponents(SecurityContext.NONE)).isFalse();
}
@Test
void showComponentsWhenShowDetailsIsNeverReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet());
assertThat(group.showComponents(SecurityContext.NONE)).isFalse();
}
@Test
void showComponentsWhenShowDetailsIsAlwaysReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet());
assertThat(group.showComponents(SecurityContext.NONE)).isTrue();
}
@Test
void showComponentsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(group.showComponents(this.securityContext)).isFalse();
}
@Test
void showComponentsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showComponents(this.securityContext)).isTrue();
}
@Test
void showComponentsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "root", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.showComponents(this.securityContext)).isTrue();
}
@Test
void showComponentsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "rot", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.showComponents(this.securityContext)).isFalse();
}
@Test
void getStatusAggregatorReturnsStatusAggregator() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.getStatusAggregator()).isSameAs(this.statusAggregator);
}
@Test
void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper);
}

@ -89,12 +89,13 @@ class AutoConfiguredHealthEndpointGroupsTests {
@Test
void createWhenNoDefinedBeansAdaptsProperties() {
this.contextRunner.withPropertyValues("management.endpoint.health.show-details=always",
"management.endpoint.health.status.order=up,down",
this.contextRunner.withPropertyValues("management.endpoint.health.show-components=always",
"management.endpoint.health.show-details=never", "management.endpoint.health.status.order=up,down",
"management.endpoint.health.status.http-mapping.down=200").run((context) -> {
HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class);
HealthEndpointGroup primary = groups.getPrimary();
assertThat(primary.includeDetails(SecurityContext.NONE)).isTrue();
assertThat(primary.showComponents(SecurityContext.NONE)).isTrue();
assertThat(primary.showDetails(SecurityContext.NONE)).isFalse();
assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
.isEqualTo(Status.UP);
assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200);

@ -82,8 +82,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
@Override
protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames);
StatusAggregator statusAggregator, boolean showComponents, Set<String> groupNames) {
return getCompositeHealth(apiVersion, contributions, statusAggregator, showComponents, groupNames);
}
}

@ -35,12 +35,20 @@ public interface HealthEndpointGroup {
boolean isMember(String name);
/**
* Returns if {@link Health#getDetails() health details} should be included in the
* Returns if {@link CompositeHealth#getComponents() health components} should be
* shown in the response.
* @param securityContext the endpoint security context
* @return {@code true} to shown details or {@code false} to hide them
*/
boolean showComponents(SecurityContext securityContext);
/**
* Returns if {@link Health#getDetails() health details} should be shown in the
* response.
* @param securityContext the endpoint security context
* @return {@code true} to included details or {@code false} to hide them
* @return {@code true} to shown details or {@code false} to hide them
*/
boolean includeDetails(SecurityContext securityContext);
boolean showDetails(SecurityContext securityContext);
/**
* Returns the status aggregator that should be used for this group.

@ -60,25 +60,25 @@ abstract class HealthEndpointSupport<C, T> {
this.groups = groups;
}
HealthResult<T> getHealth(ApiVersion apiVersion, SecurityContext securityContext, boolean alwaysIncludeDetails,
String... path) {
HealthResult<T> getHealth(ApiVersion apiVersion, SecurityContext securityContext, boolean showAll, String... path) {
HealthEndpointGroup group = (path.length > 0) ? this.groups.get(path[0]) : null;
if (group != null) {
return getHealth(apiVersion, group, securityContext, alwaysIncludeDetails, path, 1);
return getHealth(apiVersion, group, securityContext, showAll, path, 1);
}
return getHealth(apiVersion, this.groups.getPrimary(), securityContext, alwaysIncludeDetails, path, 0);
return getHealth(apiVersion, this.groups.getPrimary(), securityContext, showAll, path, 0);
}
private HealthResult<T> getHealth(ApiVersion apiVersion, HealthEndpointGroup group, SecurityContext securityContext,
boolean alwaysIncludeDetails, String[] path, int pathOffset) {
boolean includeDetails = alwaysIncludeDetails || group.includeDetails(securityContext);
boolean showAll, String[] path, int pathOffset) {
boolean showComponents = showAll || group.showComponents(securityContext);
boolean showDetails = showAll || group.showDetails(securityContext);
boolean isSystemHealth = group == this.groups.getPrimary() && pathOffset == 0;
boolean isRoot = path.length - pathOffset == 0;
if (!includeDetails && !isRoot) {
if (!showComponents && !isRoot) {
return null;
}
Object contributor = getContributor(path, pathOffset);
T health = getContribution(apiVersion, group, contributor, includeDetails,
T health = getContribution(apiVersion, group, contributor, showComponents, showDetails,
isSystemHealth ? this.groups.getNames() : null);
return (health != null) ? new HealthResult<T>(health, group) : null;
}
@ -98,46 +98,47 @@ abstract class HealthEndpointSupport<C, T> {
@SuppressWarnings("unchecked")
private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor,
boolean includeDetails, Set<String> groupNames) {
boolean showComponents, boolean showDetails, Set<String> groupNames) {
if (contributor instanceof NamedContributors) {
return getAggregateHealth(apiVersion, group, (NamedContributors<C>) contributor, includeDetails,
groupNames);
return getAggregateHealth(apiVersion, group, (NamedContributors<C>) contributor, showComponents,
showDetails, groupNames);
}
return (contributor != null) ? getHealth((C) contributor, includeDetails) : null;
return (contributor != null) ? getHealth((C) contributor, showDetails) : null;
}
private T getAggregateHealth(ApiVersion apiVersion, HealthEndpointGroup group,
NamedContributors<C> namedContributors, boolean includeDetails, Set<String> groupNames) {
NamedContributors<C> namedContributors, boolean showComponents, boolean showDetails,
Set<String> groupNames) {
Map<String, T> contributions = new LinkedHashMap<>();
for (NamedContributor<C> namedContributor : namedContributors) {
String name = namedContributor.getName();
if (group.isMember(name)) {
T contribution = getContribution(apiVersion, group, namedContributor.getContributor(), includeDetails,
null);
T contribution = getContribution(apiVersion, group, namedContributor.getContributor(), showComponents,
showDetails, null);
contributions.put(name, contribution);
}
}
if (contributions.isEmpty()) {
return null;
}
return aggregateContributions(apiVersion, contributions, group.getStatusAggregator(), includeDetails,
return aggregateContributions(apiVersion, contributions, group.getStatusAggregator(), showComponents,
groupNames);
}
protected abstract T getHealth(C contributor, boolean includeDetails);
protected abstract T aggregateContributions(ApiVersion apiVersion, Map<String, T> contributions,
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames);
StatusAggregator statusAggregator, boolean showComponents, Set<String> groupNames);
protected final CompositeHealth getCompositeHealth(ApiVersion apiVersion, Map<String, HealthComponent> components,
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
StatusAggregator statusAggregator, boolean showComponents, Set<String> groupNames) {
Status status = statusAggregator.getAggregateStatus(
components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet()));
Map<String, HealthComponent> includedComponents = includeDetails ? components : null;
Map<String, HealthComponent> instances = showComponents ? components : null;
if (groupNames != null) {
return new SystemHealth(apiVersion, status, includedComponents, groupNames);
return new SystemHealth(apiVersion, status, instances, groupNames);
}
return new CompositeHealth(apiVersion, status, includedComponents);
return new CompositeHealth(apiVersion, status, instances);
}
/**

@ -76,8 +76,8 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
}
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext,
boolean alwaysIncludeDetails, String... path) {
HealthResult<HealthComponent> result = getHealth(apiVersion, securityContext, alwaysIncludeDetails, path);
boolean showAll, String... path) {
HealthResult<HealthComponent> result = getHealth(apiVersion, securityContext, showAll, path);
if (result == null) {
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
}
@ -94,8 +94,8 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
@Override
protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames);
StatusAggregator statusAggregator, boolean showComponents, Set<String> groupNames) {
return getCompositeHealth(apiVersion, contributions, statusAggregator, showComponents, groupNames);
}
}

@ -78,9 +78,8 @@ public class ReactiveHealthEndpointWebExtension
}
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) {
HealthResult<Mono<? extends HealthComponent>> result = getHealth(apiVersion, securityContext,
alwaysIncludeDetails, path);
SecurityContext securityContext, boolean showAll, String... path) {
HealthResult<Mono<? extends HealthComponent>> result = getHealth(apiVersion, securityContext, showAll, path);
if (result == null) {
return Mono.just(new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND));
}
@ -99,10 +98,10 @@ public class ReactiveHealthEndpointWebExtension
@Override
protected Mono<? extends HealthComponent> aggregateContributions(ApiVersion apiVersion,
Map<String, Mono<? extends HealthComponent>> contributions, StatusAggregator statusAggregator,
boolean includeDetails, Set<String> groupNames) {
boolean showComponents, Set<String> groupNames) {
return Flux.fromIterable(contributions.entrySet()).flatMap(NamedHealthComponent::create)
.collectMap(NamedHealthComponent::getName, NamedHealthComponent::getHealth).map((components) -> this
.getCompositeHealth(apiVersion, components, statusAggregator, includeDetails, groupNames));
.getCompositeHealth(apiVersion, components, statusAggregator, showComponents, groupNames));
}
/**

@ -76,7 +76,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenPathIsEmptyUsesPrimaryGroup() {
void getHealthWhenPathIsEmptyUsesPrimaryGroup() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false);
@ -86,7 +86,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenPathIsNotGroupReturnsResultFromPrimaryGroup() {
void getHealthWhenPathIsNotGroupReturnsResultFromPrimaryGroup() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false, "test");
@ -96,7 +96,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenPathIsGroupReturnsResultFromGroup() {
void getHealthWhenPathIsGroupReturnsResultFromGroup() {
this.registry.registerContributor("atest", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false, "alltheas", "atest");
@ -105,7 +105,44 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsTrueIncludesDetails() {
void getHealthWhenAlwaysShowIsFalseAndGroupIsTrueShowsComponents() {
C contributor = createContributor(this.up);
C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor));
this.registry.registerContributor("test", compositeContributor);
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false, "test");
CompositeHealth health = (CompositeHealth) getHealth(result);
assertThat(health.getComponents()).containsKey("spring");
}
@Test
void getHealthWhenAlwaysShowIsFalseAndGroupIsFalseCannotAccessComponent() {
this.primaryGroup.setShowComponents(false);
C contributor = createContributor(this.up);
C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor));
this.registry.registerContributor("test", compositeContributor);
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).isNullOrEmpty();
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test");
assertThat(componentResult).isNull();
}
@Test
void getHealthWhenAlwaysShowIsTrueShowsComponents() {
this.primaryGroup.setShowComponents(true);
C contributor = createContributor(this.up);
C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor));
this.registry.registerContributor("test", compositeContributor);
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).containsKey("test");
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test");
assertThat(((CompositeHealth) getHealth(componentResult)).getComponents()).containsKey("spring");
}
@Test
void getHealthWhenAlwaysShowIsFalseAndGroupIsTrueShowsDetails() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false, "test");
@ -113,8 +150,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsFalseIncludesNoDetails() {
this.primaryGroup.setIncludeDetails(false);
void getHealthWhenAlwaysShowIsFalseAndGroupIsFalseShowsNoDetails() {
this.primaryGroup.setShowDetails(false);
this.registry.registerContributor("test", createContributor(this.up));
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
@ -124,8 +161,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenAlwaysIncludesDetailsIsTrueIncludesDetails() {
this.primaryGroup.setIncludeDetails(false);
void getHealthWhenAlwaysShowIsTrueShowsDetails() {
this.primaryGroup.setShowDetails(false);
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, true,
"test");
@ -133,7 +170,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenCompositeReturnsAggregateResult() {
void getHealthWhenCompositeReturnsAggregateResult() {
Map<String, C> contributors = new LinkedHashMap<>();
contributors.put("a", createContributor(this.up));
contributors.put("b", createContributor(this.down));
@ -148,14 +185,14 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenPathDoesNotExistReturnsNull() {
void getHealthWhenPathDoesNotExistReturnsNull() {
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false, "missing");
assertThat(result).isNull();
}
@Test
void getHealthResultWhenPathIsEmptyIncludesGroups() {
void getHealthWhenPathIsEmptyIncludesGroups() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false);
@ -163,7 +200,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthResultWhenPathIsGroupDoesNotIncludesGroups() {
void getHealthWhenPathIsGroupDoesNotIncludesGroups() {
this.registry.registerContributor("atest", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
false, "alltheas");

@ -33,7 +33,9 @@ class TestHealthEndpointGroup implements HealthEndpointGroup {
private final Predicate<String> memberPredicate;
private boolean includeDetails = true;
private Boolean showComponents;
private boolean showDetails = true;
TestHealthEndpointGroup() {
this((name) -> true);
@ -49,12 +51,21 @@ class TestHealthEndpointGroup implements HealthEndpointGroup {
}
@Override
public boolean includeDetails(SecurityContext securityContext) {
return this.includeDetails;
public boolean showComponents(SecurityContext securityContext) {
return (this.showComponents != null) ? this.showComponents : this.showDetails;
}
void setShowComponents(Boolean showComponents) {
this.showComponents = showComponents;
}
@Override
public boolean showDetails(SecurityContext securityContext) {
return this.showDetails;
}
void setIncludeDetails(boolean includeDetails) {
this.includeDetails = includeDetails;
void setShowDetails(boolean includeDetails) {
this.showDetails = includeDetails;
}
@Override

@ -623,7 +623,7 @@ The `@Endpoint` and `@WebEndpoint` annotations should be preferred whenever poss
=== Health Information
You can use health information to check the status of your running application.
It is often used by monitoring software to alert someone when a production system goes down.
The information exposed by the `health` endpoint depends on the `management.endpoint.health.show-details` property which can be configured with one of the following values:
The information exposed by the `health` endpoint depends on the `management.endpoint.health.show-details` and `management.endpoint.health.show-components` properties which can be configured with one of the following values:
[cols="1, 3"]
|===

Loading…
Cancel
Save