From a94ab673a3230e89403c1046837359c300f8717c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 25 Sep 2019 13:45:25 -0700 Subject: [PATCH] Add health endpoint 'show-components' support Add a `show-components` property under `management.endpoint.health` and `management.endpoint.health.group.` 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 --- .../src/main/asciidoc/endpoints/health.adoc | 3 + .../AutoConfiguredHealthEndpointGroup.java | 30 +++-- .../AutoConfiguredHealthEndpointGroups.java | 17 +-- .../health/HealthProperties.java | 29 +++-- .../HealthEndpointDocumentationTests.java | 7 +- ...utoConfiguredHealthEndpointGroupTests.java | 108 ++++++++++++++---- ...toConfiguredHealthEndpointGroupsTests.java | 7 +- .../boot/actuate/health/HealthEndpoint.java | 4 +- .../actuate/health/HealthEndpointGroup.java | 14 ++- .../actuate/health/HealthEndpointSupport.java | 43 +++---- .../health/HealthEndpointWebExtension.java | 8 +- .../ReactiveHealthEndpointWebExtension.java | 9 +- .../health/HealthEndpointSupportTests.java | 61 ++++++++-- .../health/TestHealthEndpointGroup.java | 21 +++- .../asciidoc/production-ready-features.adoc | 2 +- 15 files changed, 261 insertions(+), 102 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc index 3d55e176a6..a7c1998dde 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc @@ -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]] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java index 8922d148c4..52c44bd282 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java @@ -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 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 members, StatusAggregator statusAggregator, - HttpCodeStatusMapper httpCodeStatusMapper, ShowDetails showDetails, Collection roles) { + HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails, + Collection 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) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java index 30a2e78c9f..71dc2d575b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java @@ -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 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 createGroups(Map groupProperties, BeanFactory beanFactory, StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper, - ShowDetails defaultShowDetails, Set defaultRoles) { + Show defaultShowComponents, Show defaultShowDetails, Set defaultRoles) { Map groups = new LinkedHashMap(); 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 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 members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude()); groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper, - showDetails, roles)); + showComponents, showDetails, roles)); }); return Collections.unmodifiableMap(groups); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java index 9ed9bbcf6f..901c4031d5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java @@ -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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java index ea39a03ba6..588bbe881a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java @@ -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; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java index e1fe2ea748..8a31c44361 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java @@ -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); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java index d5ee14ecda..b63154031a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java @@ -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); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java index a0ffb82246..54efc0813b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java @@ -82,8 +82,8 @@ public class HealthEndpoint extends HealthEndpointSupport contributions, - StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { - return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames); + StatusAggregator statusAggregator, boolean showComponents, Set groupNames) { + return getCompositeHealth(apiVersion, contributions, statusAggregator, showComponents, groupNames); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java index f8b0531756..ec8a29035a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java @@ -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. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java index ae8a8980fc..c82e620138 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java @@ -60,25 +60,25 @@ abstract class HealthEndpointSupport { this.groups = groups; } - HealthResult getHealth(ApiVersion apiVersion, SecurityContext securityContext, boolean alwaysIncludeDetails, - String... path) { + HealthResult 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 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(health, group) : null; } @@ -98,46 +98,47 @@ abstract class HealthEndpointSupport { @SuppressWarnings("unchecked") private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor, - boolean includeDetails, Set groupNames) { + boolean showComponents, boolean showDetails, Set groupNames) { if (contributor instanceof NamedContributors) { - return getAggregateHealth(apiVersion, group, (NamedContributors) contributor, includeDetails, - groupNames); + return getAggregateHealth(apiVersion, group, (NamedContributors) 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 namedContributors, boolean includeDetails, Set groupNames) { + NamedContributors namedContributors, boolean showComponents, boolean showDetails, + Set groupNames) { Map contributions = new LinkedHashMap<>(); for (NamedContributor 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 contributions, - StatusAggregator statusAggregator, boolean includeDetails, Set groupNames); + StatusAggregator statusAggregator, boolean showComponents, Set groupNames); protected final CompositeHealth getCompositeHealth(ApiVersion apiVersion, Map components, - StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { + StatusAggregator statusAggregator, boolean showComponents, Set groupNames) { Status status = statusAggregator.getAggregateStatus( components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet())); - Map includedComponents = includeDetails ? components : null; + Map 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); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java index a3bf307305..499da09f9e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java @@ -76,8 +76,8 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport health(ApiVersion apiVersion, SecurityContext securityContext, - boolean alwaysIncludeDetails, String... path) { - HealthResult result = getHealth(apiVersion, securityContext, alwaysIncludeDetails, path); + boolean showAll, String... path) { + HealthResult 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 contributions, - StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { - return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames); + StatusAggregator statusAggregator, boolean showComponents, Set groupNames) { + return getCompositeHealth(apiVersion, contributions, statusAggregator, showComponents, groupNames); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java index 75f933bc19..5f2122da1f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java @@ -78,9 +78,8 @@ public class ReactiveHealthEndpointWebExtension } public Mono> health(ApiVersion apiVersion, - SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { - HealthResult> result = getHealth(apiVersion, securityContext, - alwaysIncludeDetails, path); + SecurityContext securityContext, boolean showAll, String... path) { + HealthResult> 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 aggregateContributions(ApiVersion apiVersion, Map> contributions, StatusAggregator statusAggregator, - boolean includeDetails, Set groupNames) { + boolean showComponents, Set 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)); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java index 3362f6ada3..60e1421b38 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java @@ -76,7 +76,7 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenPathIsEmptyUsesPrimaryGroup() { + void getHealthWhenPathIsEmptyUsesPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false); @@ -86,7 +86,7 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenPathIsNotGroupReturnsResultFromPrimaryGroup() { + void getHealthWhenPathIsNotGroupReturnsResultFromPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test"); @@ -96,7 +96,7 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenPathIsGroupReturnsResultFromGroup() { + void getHealthWhenPathIsGroupReturnsResultFromGroup() { this.registry.registerContributor("atest", createContributor(this.up)); HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false, "alltheas", "atest"); @@ -105,7 +105,44 @@ abstract class HealthEndpointSupportTests, 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 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 endpoint = create(this.registry, this.groups); + HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); + assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).isNullOrEmpty(); + HealthResult 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 endpoint = create(this.registry, this.groups); + HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); + assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).containsKey("test"); + HealthResult 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 result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test"); @@ -113,8 +150,8 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsFalseIncludesNoDetails() { - this.primaryGroup.setIncludeDetails(false); + void getHealthWhenAlwaysShowIsFalseAndGroupIsFalseShowsNoDetails() { + this.primaryGroup.setShowDetails(false); this.registry.registerContributor("test", createContributor(this.up)); HealthEndpointSupport endpoint = create(this.registry, this.groups); HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); @@ -124,8 +161,8 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenAlwaysIncludesDetailsIsTrueIncludesDetails() { - this.primaryGroup.setIncludeDetails(false); + void getHealthWhenAlwaysShowIsTrueShowsDetails() { + this.primaryGroup.setShowDetails(false); this.registry.registerContributor("test", createContributor(this.up)); HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, true, "test"); @@ -133,7 +170,7 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenCompositeReturnsAggregateResult() { + void getHealthWhenCompositeReturnsAggregateResult() { Map contributors = new LinkedHashMap<>(); contributors.put("a", createContributor(this.up)); contributors.put("b", createContributor(this.down)); @@ -148,14 +185,14 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenPathDoesNotExistReturnsNull() { + void getHealthWhenPathDoesNotExistReturnsNull() { HealthResult 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 result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false); @@ -163,7 +200,7 @@ abstract class HealthEndpointSupportTests, C, T } @Test - void getHealthResultWhenPathIsGroupDoesNotIncludesGroups() { + void getHealthWhenPathIsGroupDoesNotIncludesGroups() { this.registry.registerContributor("atest", createContributor(this.up)); HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false, "alltheas"); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java index cc8c6cab56..eb041bf908 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java @@ -33,7 +33,9 @@ class TestHealthEndpointGroup implements HealthEndpointGroup { private final Predicate 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 setIncludeDetails(boolean includeDetails) { - this.includeDetails = includeDetails; + void setShowComponents(Boolean showComponents) { + this.showComponents = showComponents; + } + + @Override + public boolean showDetails(SecurityContext securityContext) { + return this.showDetails; + } + + void setShowDetails(boolean includeDetails) { + this.showDetails = includeDetails; } @Override diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 68f4bc0502..9b374db724 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -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"] |===