From 71dcb84e618e98cc604e91398df129ba01df2ae5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 20 Aug 2019 14:09:04 -0700 Subject: [PATCH] Add support for health indicator groups Update the `HealthEndpoint` to support health groups. The `HealthEndpointSettings` interface has been replaced with `HealthEndpointGroups` which provides access to the primary group as well as an optional set of additional groups. Groups can be configured via properties and may have custom `StatusAggregator` and `HttpCodeStatusMapper` settings. Closes gh-14022 Co-authored-by: Stephane Nicoll --- ...toConfiguredHealthContributorRegistry.java | 55 +++ ...=> AutoConfiguredHealthEndpointGroup.java} | 25 +- .../AutoConfiguredHealthEndpointGroups.java | 155 ++++++++ ...uredReactiveHealthContributorRegistry.java | 55 +++ .../health/HealthEndpointConfiguration.java | 26 +- .../health/HealthEndpointProperties.java | 90 +---- ...ointReactiveWebExtensionConfiguration.java | 6 +- ...althEndpointWebExtensionConfiguration.java | 6 +- .../health/HealthProperties.java | 124 ++++++ .../ReactiveHealthEndpointConfiguration.java | 6 +- ...loudFoundryWebEndpointDiscovererTests.java | 6 +- .../HealthEndpointDocumentationTests.java | 16 +- ...figuredHealthContributorRegistryTests.java | 55 +++ ...utoConfiguredHealthEndpointGroupTests.java | 140 +++++++ ...toConfiguredHealthEndpointGroupsTests.java | 366 ++++++++++++++++++ ...ConfiguredHealthEndpointSettingsTests.java | 122 ------ ...eactiveHealthContributorRegistryTests.java | 55 +++ .../HealthEndpointAutoConfigurationTests.java | 35 +- .../boot/actuate/health/HealthEndpoint.java | 16 +- ...Settings.java => HealthEndpointGroup.java} | 18 +- .../actuate/health/HealthEndpointGroups.java | 80 ++++ .../actuate/health/HealthEndpointSupport.java | 88 +++-- .../health/HealthEndpointWebExtension.java | 21 +- .../ReactiveHealthEndpointWebExtension.java | 21 +- .../boot/actuate/health/SystemHealth.java | 47 +++ .../health/HealthEndpointGroupsTests.java | 58 +++ .../health/HealthEndpointSupportTests.java | 80 ++-- .../actuate/health/HealthEndpointTests.java | 20 +- .../HealthEndpointWebExtensionTests.java | 22 +- .../HealthEndpointWebIntegrationTests.java | 18 +- ...activeHealthEndpointWebExtensionTests.java | 17 +- .../actuate/health/SystemHealthTests.java | 51 +++ ...ings.java => TestHealthEndpointGroup.java} | 21 +- .../asciidoc/production-ready-features.adoc | 36 ++ .../src/main/resources/application.properties | 5 + ...AndPathSampleActuatorApplicationTests.java | 2 +- 36 files changed, 1598 insertions(+), 366 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistry.java rename spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/{AutoConfiguredHealthEndpointSettings.java => AutoConfiguredHealthEndpointGroup.java} (76%) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistry.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistryTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettingsTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistryTests.java rename spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/{HealthEndpointSettings.java => HealthEndpointGroup.java} (71%) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointGroupsTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java rename spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/{TestHealthEndpointSettings.java => TestHealthEndpointGroup.java} (74%) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistry.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistry.java new file mode 100644 index 000000000000..6d0121715b87 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistry.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.HealthContributorRegistry; +import org.springframework.util.Assert; + +/** + * An auto-configured {@link HealthContributorRegistry} that ensures registered indicators + * do not clash with groups names. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthContributorRegistry extends DefaultHealthContributorRegistry { + + private final Collection groupNames; + + AutoConfiguredHealthContributorRegistry(Map contributors, + Collection groupNames) { + super(contributors); + this.groupNames = groupNames; + contributors.keySet().forEach(this::assertDoesNotClashWithGroup); + } + + @Override + public void registerContributor(String name, HealthContributor contributor) { + assertDoesNotClashWithGroup(name); + super.registerContributor(name, contributor); + } + + private void assertDoesNotClashWithGroup(String name) { + Assert.state(!this.groupNames.contains(name), + () -> "HealthContributor with name \"" + name + "\" clashes with group"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettings.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java similarity index 76% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettings.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java index 47dc3c845caf..8922d148c400 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettings.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java @@ -17,22 +17,24 @@ package org.springframework.boot.actuate.autoconfigure.health; import java.util.Collection; +import java.util.function.Predicate; -import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.ShowDetails; +import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.util.CollectionUtils; /** - * Auto-configured {@link HealthEndpointSettings} backed by - * {@link HealthEndpointProperties}. + * Auto-configured {@link HealthEndpointGroup} backed by {@link HealthProperties}. * * @author Phillip Webb * @author Andy Wilkinson */ -class AutoConfiguredHealthEndpointSettings implements HealthEndpointSettings { +class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { + + private final Predicate members; private final StatusAggregator statusAggregator; @@ -43,20 +45,27 @@ class AutoConfiguredHealthEndpointSettings implements HealthEndpointSettings { private final Collection roles; /** - * Create a new {@link AutoConfiguredHealthEndpointSettings} instance. + * Create a new {@link AutoConfiguredHealthEndpointGroup} instance. + * @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 showDetails the show details setting * @param roles the roles to match */ - AutoConfiguredHealthEndpointSettings(StatusAggregator statusAggregator, HttpCodeStatusMapper httpCodeStatusMapper, - ShowDetails showDetails, Collection roles) { + AutoConfiguredHealthEndpointGroup(Predicate members, StatusAggregator statusAggregator, + HttpCodeStatusMapper httpCodeStatusMapper, ShowDetails showDetails, Collection roles) { + this.members = members; this.statusAggregator = statusAggregator; this.httpCodeStatusMapper = httpCodeStatusMapper; this.showDetails = showDetails; this.roles = roles; } + @Override + public boolean isMember(String name) { + return this.members.test(name); + } + @Override public boolean includeDetails(SecurityContext securityContext) { ShowDetails showDetails = this.showDetails; 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 new file mode 100644 index 000000000000..30a2e78c9f88 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +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.Status; +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.actuate.health.HttpCodeStatusMapper; +import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; +import org.springframework.boot.actuate.health.SimpleStatusAggregator; +import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * Auto-configured {@link HealthEndpointGroups}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups { + + private static Predicate ALL = (name) -> true; + + private final HealthEndpointGroup primaryGroup; + + private final Map groups; + + /** + * Create a new {@link AutoConfiguredHealthEndpointGroups} instance. + * @param applicationContext the application context used to check for override beans + * @param properties the health endpoint properties + */ + AutoConfiguredHealthEndpointGroups(ApplicationContext applicationContext, HealthEndpointProperties properties) { + ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext) + ? ((ConfigurableApplicationContext) applicationContext).getBeanFactory() : applicationContext; + ShowDetails showDetails = properties.getShowDetails(); + Set roles = properties.getRoles(); + StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class); + if (statusAggregator == null) { + statusAggregator = new SimpleStatusAggregator(properties.getStatus().getOrder()); + } + HttpCodeStatusMapper httpCodeStatusMapper = getNonQualifiedBean(beanFactory, HttpCodeStatusMapper.class); + if (httpCodeStatusMapper == null) { + httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping()); + } + this.primaryGroup = new AutoConfiguredHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper, + showDetails, roles); + this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, httpCodeStatusMapper, + showDetails, roles); + } + + private Map createGroups(Map groupProperties, BeanFactory beanFactory, + StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper, + ShowDetails defaultShowDetails, Set defaultRoles) { + Map groups = new LinkedHashMap(); + groupProperties.forEach((groupName, group) -> { + Status status = group.getStatus(); + ShowDetails 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())) { + return new SimpleStatusAggregator(status.getOrder()); + } + return defaultStatusAggregator; + }); + HttpCodeStatusMapper httpCodeStatusMapper = getQualifiedBean(beanFactory, HttpCodeStatusMapper.class, + groupName, () -> { + if (!CollectionUtils.isEmpty(status.getHttpMapping())) { + return new SimpleHttpCodeStatusMapper(status.getHttpMapping()); + } + return defaultHttpCodeStatusMapper; + }); + Predicate members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude()); + groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper, + showDetails, roles)); + }); + return Collections.unmodifiableMap(groups); + } + + private T getNonQualifiedBean(ListableBeanFactory beanFactory, Class type) { + List candidates = new ArrayList<>(); + for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { + String[] aliases = beanFactory.getAliases(beanName); + if (!BeanFactoryAnnotationUtils.isQualifierMatch( + (qualifier) -> !qualifier.equals(beanName) && !ObjectUtils.containsElement(aliases, qualifier), + beanName, beanFactory)) { + candidates.add(beanName); + } + } + if (candidates.isEmpty()) { + return null; + } + if (candidates.size() == 1) { + return beanFactory.getBean(candidates.get(0), type); + } + return beanFactory.getBean(type); + } + + private T getQualifiedBean(BeanFactory beanFactory, Class type, String qualifier, Supplier fallback) { + try { + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, type, qualifier); + } + catch (NoSuchBeanDefinitionException ex) { + return fallback.get(); + } + } + + @Override + public HealthEndpointGroup getPrimary() { + return this.primaryGroup; + } + + @Override + public Set getNames() { + return this.groups.keySet(); + } + + @Override + public HealthEndpointGroup get(String name) { + return this.groups.get(name); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistry.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistry.java new file mode 100644 index 000000000000..f115913dfb0f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistry.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry; +import org.springframework.boot.actuate.health.HealthContributorRegistry; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.util.Assert; + +/** + * An auto-configured {@link HealthContributorRegistry} that ensures registered indicators + * do not clash with groups names. + * + * @author Phillip Webb + */ +class AutoConfiguredReactiveHealthContributorRegistry extends DefaultReactiveHealthContributorRegistry { + + private final Collection groupNames; + + AutoConfiguredReactiveHealthContributorRegistry(Map contributors, + Collection groupNames) { + super(contributors); + this.groupNames = groupNames; + contributors.keySet().forEach(this::assertDoesNotClashWithGroup); + } + + @Override + public void registerContributor(String name, ReactiveHealthContributor contributor) { + assertDoesNotClashWithGroup(name); + super.registerContributor(name, contributor); + } + + private void assertDoesNotClashWithGroup(String name) { + Assert.state(!this.groupNames.contains(name), + () -> "ReactiveHealthContributor with name \"" + name + "\" clashes with group"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java index cad0b861b024..ed7e854fa1f1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java @@ -18,17 +18,16 @@ import java.util.Map; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; import org.springframework.boot.actuate.health.SimpleStatusAggregator; import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,27 +54,22 @@ HttpCodeStatusMapper healthHttpCodeStatusMapper(HealthEndpointProperties propert @Bean @ConditionalOnMissingBean - HealthEndpointSettings healthEndpointSettings(HealthEndpointProperties properties, - ObjectProvider statusAggregatorProvider, - ObjectProvider httpCodeStatusMapperProvider) { - StatusAggregator statusAggregator = statusAggregatorProvider - .getIfAvailable(() -> new SimpleStatusAggregator(properties.getStatus().getOrder())); - HttpCodeStatusMapper httpCodeStatusMapper = httpCodeStatusMapperProvider - .getIfAvailable(() -> new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping())); - return new AutoConfiguredHealthEndpointSettings(statusAggregator, httpCodeStatusMapper, - properties.getShowDetails(), properties.getRoles()); + HealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext, + HealthEndpointProperties properties) { + return new AutoConfiguredHealthEndpointGroups(applicationContext, properties); } @Bean @ConditionalOnMissingBean - HealthContributorRegistry healthContributorRegistry(Map healthContributors) { - return new DefaultHealthContributorRegistry(healthContributors); + HealthContributorRegistry healthContributorRegistry(Map healthContributors, + HealthEndpointGroups groups) { + return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames()); } @Bean @ConditionalOnMissingBean - HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointSettings settings) { - return new HealthEndpoint(registry, settings); + HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups) { + return new HealthEndpoint(registry, groups); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java index 12dfc3b8e298..0a551f8d0378 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java @@ -16,9 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.health; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -32,93 +30,47 @@ * @since 2.0.0 */ @ConfigurationProperties("management.endpoint.health") -public class HealthEndpointProperties { - - private final Status status = new Status(); - - /** - * When to show full health details. - */ - private ShowDetails showDetails = ShowDetails.NEVER; +public class HealthEndpointProperties extends HealthProperties { /** - * Roles used to determine whether or not a user is authorized to be shown details. - * When empty, all authenticated users are authorized. + * Health endpoint groups. */ - private Set roles = new HashSet<>(); + private Map group = new LinkedHashMap<>(); - public Status getStatus() { - return this.status; - } - - public ShowDetails getShowDetails() { - return this.showDetails; - } - - public void setShowDetails(ShowDetails showDetails) { - this.showDetails = showDetails; - } - - public Set getRoles() { - return this.roles; - } - - public void setRoles(Set roles) { - this.roles = roles; + public Map getGroup() { + return this.group; } /** - * Status properties for the group. + * A health endpoint group. */ - public static class Status { + public static class Group extends HealthProperties { /** - * Comma-separated list of health statuses in order of severity. + * The health indicator IDs to include. Use '*' if you want to include all. */ - private List order = null; + private Set include; /** - * Mapping of health statuses to HTTP status codes. By default, registered health - * statuses map to sensible defaults (for example, UP maps to 200). + * The health indicator IDs to exclude. Use '*' if you want to exclude all. */ - private final Map httpMapping = new HashMap<>(); + private Set exclude; - public List getOrder() { - return this.order; + public Set getInclude() { + return this.include; } - public void setOrder(List statusOrder) { - if (statusOrder != null && !statusOrder.isEmpty()) { - this.order = statusOrder; - } + public void setInclude(Set include) { + this.include = include; } - public Map getHttpMapping() { - return this.httpMapping; + public Set getExclude() { + return this.exclude; } - } - - /** - * Options for showing details in responses from the {@link HealthEndpoint} web - * extensions. - */ - public enum ShowDetails { - - /** - * Never show details in the response. - */ - NEVER, - - /** - * Show details in the response when accessed by an authorized user. - */ - WHEN_AUTHORIZED, - - /** - * Always show details in the response. - */ - ALWAYS + public void setExclude(Set exclude) { + this.exclude = exclude; + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java index 38d9168b552c..d556e1904c62 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java @@ -17,7 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.health; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -42,8 +42,8 @@ class HealthEndpointReactiveWebExtensionConfiguration { @ConditionalOnMissingBean @ConditionalOnBean(HealthEndpoint.class) ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension( - ReactiveHealthContributorRegistry reactiveHealthContributorRegistry, HealthEndpointSettings settings) { - return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, settings); + ReactiveHealthContributorRegistry reactiveHealthContributorRegistry, HealthEndpointGroups groups) { + return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, groups); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index 1b2221427ed5..777c07551c17 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -42,8 +42,8 @@ class HealthEndpointWebExtensionConfiguration { @ConditionalOnBean(HealthEndpoint.class) @ConditionalOnMissingBean HealthEndpointWebExtension healthEndpointWebExtension(HealthContributorRegistry healthContributorRegistry, - HealthEndpointSettings settings) { - return new HealthEndpointWebExtension(healthContributorRegistry, settings); + HealthEndpointGroups groups) { + return new HealthEndpointWebExtension(healthContributorRegistry, 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 new file mode 100644 index 000000000000..53e2bc01d391 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.actuate.health.HealthEndpoint; + +/** + * Properties used to configure the health endpoint and endpoint groups. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 2.2.0 + */ +public abstract class HealthProperties { + + private final Status status = new Status(); + + /** + * When to show full health details. + */ + private ShowDetails showDetails = ShowDetails.NEVER; + + /** + * Roles used to determine whether or not a user is authorized to be shown details. + * When empty, all authenticated users are authorized. + */ + private Set roles = new HashSet<>(); + + public Status getStatus() { + return this.status; + } + + public ShowDetails getShowDetails() { + return this.showDetails; + } + + public void setShowDetails(ShowDetails showDetails) { + this.showDetails = showDetails; + } + + public Set getRoles() { + return this.roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + /** + * Status properties for the group. + */ + public static class Status { + + /** + * Comma-separated list of health statuses in order of severity. + */ + private List order = null; + + /** + * Mapping of health statuses to HTTP status codes. By default, registered health + * statuses map to sensible defaults (for example, UP maps to 200). + */ + private final Map httpMapping = new HashMap<>(); + + public List getOrder() { + return this.order; + } + + public void setOrder(List statusOrder) { + if (statusOrder != null && !statusOrder.isEmpty()) { + this.order = statusOrder; + } + } + + public Map getHttpMapping() { + return this.httpMapping; + } + + } + + /** + * Options for showing details in responses from the {@link HealthEndpoint} web + * extensions. + */ + public enum ShowDetails { + + /** + * Never show details in the response. + */ + NEVER, + + /** + * Show details in the response when accessed by an authorized user. + */ + WHEN_AUTHORIZED, + + /** + * Always show details in the response. + */ + ALWAYS + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointConfiguration.java index 624ed26f6465..b41938a72116 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointConfiguration.java @@ -21,9 +21,9 @@ import reactor.core.publisher.Flux; -import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.ReactiveHealthContributor; import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -47,11 +47,11 @@ class ReactiveHealthEndpointConfiguration { @ConditionalOnMissingBean ReactiveHealthContributorRegistry reactiveHealthContributorRegistry( Map healthContributors, - Map reactiveHealthContributors) { + Map reactiveHealthContributors, HealthEndpointGroups groups) { Map allContributors = new LinkedHashMap<>(reactiveHealthContributors); healthContributors.forEach((name, contributor) -> allContributors.computeIfAbsent(name, (key) -> ReactiveHealthContributor.adapt(contributor))); - return new DefaultReactiveHealthContributorRegistry(allContributors); + return new AutoConfiguredReactiveHealthContributorRegistry(allContributors, groups.getNames()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java index 84b371eab0eb..bc487df92857 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java @@ -37,7 +37,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -112,8 +112,8 @@ TestEndpointWebExtension testEndpointWebExtension() { @Bean HealthEndpoint healthEndpoint() { HealthContributorRegistry registry = mock(HealthContributorRegistry.class); - HealthEndpointSettings settings = mock(HealthEndpointSettings.class); - return new HealthEndpoint(registry, settings); + HealthEndpointGroups groups = mock(HealthEndpointGroups.class); + return new HealthEndpoint(registry, groups); } @Bean 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 f1a432c29733..2e6bb98245c3 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 @@ -18,6 +18,7 @@ import java.io.File; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,8 @@ import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; @@ -104,8 +106,9 @@ static class TestConfiguration { @Bean HealthEndpoint healthEndpoint(Map healthContributors) { HealthContributorRegistry registry = new DefaultHealthContributorRegistry(healthContributors); - HealthEndpointSettings settings = new TestHealthEndpointSettings(); - return new HealthEndpoint(registry, settings); + HealthEndpointGroup primary = new TestHealthEndpointGroup(); + HealthEndpointGroups groups = HealthEndpointGroups.of(primary, Collections.emptyMap()); + return new HealthEndpoint(registry, groups); } @Bean @@ -128,12 +131,17 @@ CompositeHealthContributor brokerHealthContributor() { } - private static class TestHealthEndpointSettings implements HealthEndpointSettings { + private static class TestHealthEndpointGroup implements HealthEndpointGroup { private final StatusAggregator statusAggregator = new SimpleStatusAggregator(); private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); + @Override + public boolean isMember(String name) { + return true; + } + @Override public boolean includeDetails(SecurityContext securityContext) { return true; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistryTests.java new file mode 100644 index 000000000000..d17a8e861253 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthContributorRegistryTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.HealthContributorRegistry; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AutoConfiguredHealthContributorRegistry}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthContributorRegistryTests { + + @Test + void createWhenContributorsClashesWithGroupNameThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new AutoConfiguredHealthContributorRegistry( + Collections.singletonMap("boot", mock(HealthContributor.class)), + Arrays.asList("spring", "boot"))) + .withMessage("HealthContributor with name \"boot\" clashes with group"); + } + + @Test + void registerContributorWithGroupNameThrowsException() { + HealthContributorRegistry registry = new AutoConfiguredHealthContributorRegistry(Collections.emptyMap(), + Arrays.asList("spring", "boot")); + assertThatIllegalStateException() + .isThrownBy(() -> registry.registerContributor("spring", mock(HealthContributor.class))) + .withMessage("HealthContributor with name \"spring\" clashes with group"); + } + +} 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 new file mode 100644 index 000000000000..e1fe2ea7484a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +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.endpoint.SecurityContext; +import org.springframework.boot.actuate.health.HttpCodeStatusMapper; +import org.springframework.boot.actuate.health.StatusAggregator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link AutoConfiguredHealthEndpointGroup}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthEndpointGroupTests { + + @Mock + private StatusAggregator statusAggregator; + + @Mock + private HttpCodeStatusMapper httpCodeStatusMapper; + + @Mock + private SecurityContext securityContext; + + @Mock + private Principal principal; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + void isMemberWhenMemberPredicateMatchesAcceptsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"), + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); + assertThat(group.isMember("albert")).isTrue(); + assertThat(group.isMember("arnold")).isTrue(); + } + + @Test + void isMemberWhenMemberPredicateRejectsReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"), + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); + assertThat(group.isMember("bert")).isFalse(); + assertThat(group.isMember("ernie")).isFalse(); + } + + @Test + void includeDetailsWhenShowDetailsIsNeverReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.NEVER, Collections.emptySet()); + assertThat(group.includeDetails(SecurityContext.NONE)).isFalse(); + } + + @Test + void includeDetailsWhenShowDetailsIsAlwaysReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); + assertThat(group.includeDetails(SecurityContext.NONE)).isTrue(); + } + + @Test + void includeDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet()); + given(this.securityContext.getPrincipal()).willReturn(null); + assertThat(group.includeDetails(this.securityContext)).isFalse(); + } + + @Test + void includeDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet()); + given(this.securityContext.getPrincipal()).willReturn(this.principal); + assertThat(group.includeDetails(this.securityContext)).isTrue(); + } + + @Test + void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.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(); + } + + @Test + void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.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(); + } + + @Test + void getStatusAggregatorReturnsStatusAggregator() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.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()); + 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 new file mode 100644 index 000000000000..d5ee14ecda76 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java @@ -0,0 +1,366 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.actuate.health.HttpCodeStatusMapper; +import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; +import org.springframework.boot.actuate.health.SimpleStatusAggregator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfiguredHealthEndpointGroups}. + * + * @author Phillip Webb + */ +class AutoConfiguredHealthEndpointGroupsTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AutoConfiguredHealthEndpointGroupsTestConfiguration.class)); + + @Test + void getPrimaryGroupMatchesAllMembers() { + this.contextRunner.run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + assertThat(primary.isMember("a")).isTrue(); + assertThat(primary.isMember("b")).isTrue(); + assertThat(primary.isMember("C")).isTrue(); + }); + } + + @Test + void getNamesReturnsGroupNames() { + this.contextRunner.withPropertyValues("management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.b.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + assertThat(groups.getNames()).containsExactlyInAnyOrder("a", "b"); + }); + } + + @Test + void getGroupWhenGroupExistsReturnsGroup() { + this.contextRunner.withPropertyValues("management.endpoint.health.group.a.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup group = groups.get("a"); + assertThat(group).isNotNull(); + }); + } + + @Test + void getGroupWhenGroupDoesNotExistReturnsNull() { + this.contextRunner.withPropertyValues("management.endpoint.health.group.a.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup group = groups.get("b"); + assertThat(group).isNull(); + }); + } + + @Test + void createWhenNoDefinedBeansAdaptsProperties() { + this.contextRunner.withPropertyValues("management.endpoint.health.show-details=always", + "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.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN)) + .isEqualTo(Status.UP); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + }); + } + + @Test + void createWhenHasStatusAggregatorBeanReturnsInstanceWithAgregatorUsedForAllGroups() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class) + .withPropertyValues("management.endpoint.health.status.order=up,down", + "management.endpoint.health.group.a.include=*") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(groupA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + }); + } + + @Test + void createWhenHasStatusAggregatorBeanAndGroupSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredGroups() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class) + .withPropertyValues("management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.order=up,down", + "management.endpoint.health.group.b.include=*") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(groupA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + assertThat(groupB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + }); + } + + @Test + void createWhenHasStatusAggregatorPropertyReturnsInstanceWithPropertyUsedForAllGroups() { + this.contextRunner.withPropertyValues("management.endpoint.health.status.order=up,down", + "management.endpoint.health.group.a.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN)) + .isEqualTo(Status.UP); + assertThat(groupA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasStatusAggregatorPropertyAndGroupSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedGroups() { + this.contextRunner.withPropertyValues("management.endpoint.health.status.order=up,down", + "management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.order=unknown,up,down", + "management.endpoint.health.group.b.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + assertThat(groupA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(groupB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasStatusAggregatorPropertyAndGroupQualifiedBeanReturnsInstanceWithBeanUsedForExpectedGroups() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorGroupAConfiguration.class) + .withPropertyValues("management.endpoint.health.status.order=up,down", + "management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.order=up,down", + "management.endpoint.health.group.b.include=*") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + assertThat(groupA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(groupB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasGroupSpecificStatusAggregatorPropertyAndGroupQualifiedBeanReturnsInstanceWithBeanUsedForExpectedGroups() { + this.contextRunner.withUserConfiguration(CustomStatusAggregatorGroupAConfiguration.class) + .withPropertyValues("management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.order=up,down", + "management.endpoint.health.group.b.include=*", + "management.endpoint.health.group.b.status.order=up,down") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.DOWN); + assertThat(groupA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UNKNOWN); + assertThat(groupB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN)) + .isEqualTo(Status.UP); + }); + } + + @Test + void createWhenHasHttpCodeStatusMapperBeanReturnsInstanceWithMapperUsedForAllGroups() { + this.contextRunner.withUserConfiguration(CustomHttpCodeStatusMapperConfiguration.class) + .withPropertyValues("management.endpoint.health.status.http-mapping.down=201", + "management.endpoint.health.group.a.include=*") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + assertThat(groupA.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + }); + } + + @Test + void createWhenHasHttpCodeStatusMapperBeanAndGroupSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredGroups() { + this.contextRunner.withUserConfiguration(CustomHttpCodeStatusMapperConfiguration.class) + .withPropertyValues("management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.http-mapping.down=201", + "management.endpoint.health.group.b.include=*") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + assertThat(groupA.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + assertThat(groupB.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + }); + } + + @Test + void createWhenHasHttpCodeStatusMapperPropertyReturnsInstanceWithPropertyUsedForAllGroups() { + this.contextRunner.withPropertyValues("management.endpoint.health.status.http-mapping.down=201", + "management.endpoint.health.group.a.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + assertThat(groupA.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + }); + } + + @Test + void createWhenHasHttpCodeStatusMapperPropertyAndGroupSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedGroups() { + this.contextRunner.withPropertyValues("management.endpoint.health.status.http-mapping.down=201", + "management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.http-mapping.down=202", + "management.endpoint.health.group.b.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + assertThat(groupA.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(202); + assertThat(groupB.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + }); + } + + @Test + void createWhenHasHttpCodeStatusMapperPropertyAndGroupQualifiedBeanReturnsInstanceWithBeanUsedForExpectedGroups() { + this.contextRunner.withUserConfiguration(CustomHttpCodeStatusMapperGroupAConfiguration.class) + .withPropertyValues("management.endpoint.health.status.http-mapping.down=201", + "management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.http-mapping.down=201", + "management.endpoint.health.group.b.include=*") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + assertThat(groupA.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + assertThat(groupB.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + }); + } + + @Test + void createWhenHasGroupSpecificHttpCodeStatusMapperPropertyAndGroupQualifiedBeanReturnsInstanceWithBeanUsedForExpectedGroups() { + this.contextRunner.withUserConfiguration(CustomHttpCodeStatusMapperGroupAConfiguration.class) + .withPropertyValues("management.endpoint.health.group.a.include=*", + "management.endpoint.health.group.a.status.http-mapping.down=201", + "management.endpoint.health.group.b.include=*", + "management.endpoint.health.group.b.status.http-mapping.down=201") + .run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup primary = groups.getPrimary(); + HealthEndpointGroup groupA = groups.get("a"); + HealthEndpointGroup groupB = groups.get("b"); + assertThat(primary.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(503); + assertThat(groupA.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(200); + assertThat(groupB.getHttpCodeStatusMapper().getStatusCode(Status.DOWN)).isEqualTo(201); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(HealthEndpointProperties.class) + static class AutoConfiguredHealthEndpointGroupsTestConfiguration { + + @Bean + AutoConfiguredHealthEndpointGroups healthEndpointGroups(ConfigurableApplicationContext applicationContext, + HealthEndpointProperties properties) { + return new AutoConfiguredHealthEndpointGroups(applicationContext, properties); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomStatusAggregatorConfiguration { + + @Bean + @Primary + StatusAggregator statusAggregator() { + return new SimpleStatusAggregator(Status.UNKNOWN, Status.UP, Status.DOWN); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomStatusAggregatorGroupAConfiguration { + + @Bean + @Qualifier("a") + StatusAggregator statusAggregator() { + return new SimpleStatusAggregator(Status.UNKNOWN, Status.UP, Status.DOWN); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomHttpCodeStatusMapperConfiguration { + + @Bean + @Primary + HttpCodeStatusMapper httpCodeStatusMapper() { + return new SimpleHttpCodeStatusMapper(Collections.singletonMap(Status.DOWN.getCode(), 200)); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomHttpCodeStatusMapperGroupAConfiguration { + + @Bean + @Qualifier("a") + HttpCodeStatusMapper httpCodeStatusMapper() { + return new SimpleHttpCodeStatusMapper(Collections.singletonMap(Status.DOWN.getCode(), 200)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettingsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettingsTests.java deleted file mode 100644 index cc6353de561d..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointSettingsTests.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.security.Principal; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.ShowDetails; -import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.health.HttpCodeStatusMapper; -import org.springframework.boot.actuate.health.StatusAggregator; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; - -/** - * Tests for {@link AutoConfiguredHealthEndpointSettings}. - * - * @author Phillip Webb - */ -class AutoConfiguredHealthEndpointSettingsTests { - - @Mock - private StatusAggregator statusAggregator; - - @Mock - private HttpCodeStatusMapper httpCodeStatusMapper; - - @Mock - private SecurityContext securityContext; - - @Mock - private Principal principal; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - void includeDetailsWhenShowDetailsIsNeverReturnsFalse() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.NEVER, Collections.emptySet()); - assertThat(settings.includeDetails(SecurityContext.NONE)).isFalse(); - } - - @Test - void includeDetailsWhenShowDetailsIsAlwaysReturnsTrue() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); - assertThat(settings.includeDetails(SecurityContext.NONE)).isTrue(); - } - - @Test - void includeDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet()); - given(this.securityContext.getPrincipal()).willReturn(null); - assertThat(settings.includeDetails(this.securityContext)).isFalse(); - } - - @Test - void includeDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet()); - given(this.securityContext.getPrincipal()).willReturn(this.principal); - assertThat(settings.includeDetails(this.securityContext)).isTrue(); - } - - @Test - void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Arrays.asList("admin", "root", "bossmode")); - given(this.securityContext.getPrincipal()).willReturn(this.principal); - given(this.securityContext.isUserInRole("root")).willReturn(true); - assertThat(settings.includeDetails(this.securityContext)).isTrue(); - } - - @Test - void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Arrays.asList("admin", "rot", "bossmode")); - given(this.securityContext.getPrincipal()).willReturn(this.principal); - given(this.securityContext.isUserInRole("root")).willReturn(true); - assertThat(settings.includeDetails(this.securityContext)).isFalse(); - } - - @Test - void getStatusAggregatorReturnsStatusAggregator() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); - assertThat(settings.getStatusAggregator()).isSameAs(this.statusAggregator); - } - - @Test - void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() { - AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, - this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); - assertThat(settings.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistryTests.java new file mode 100644 index 000000000000..2f9482cdf829 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredReactiveHealthContributorRegistryTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AutoConfiguredReactiveHealthContributorRegistry}. + * + * @author Phillip Webb + */ +class AutoConfiguredReactiveHealthContributorRegistryTests { + + @Test + void createWhenContributorsClashesWithGroupNameThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new AutoConfiguredReactiveHealthContributorRegistry( + Collections.singletonMap("boot", mock(ReactiveHealthContributor.class)), + Arrays.asList("spring", "boot"))) + .withMessage("ReactiveHealthContributor with name \"boot\" clashes with group"); + } + + @Test + void registerContributorWithGroupNameThrowsException() { + ReactiveHealthContributorRegistry registry = new AutoConfiguredReactiveHealthContributorRegistry( + Collections.emptyMap(), Arrays.asList("spring", "boot")); + assertThatIllegalStateException() + .isThrownBy(() -> registry.registerContributor("spring", mock(ReactiveHealthContributor.class))) + .withMessage("ReactiveHealthContributor with name \"spring\" clashes with group"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java index 6ce4abd7b5b3..bc403737d2a6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java @@ -16,10 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.SecurityContext; @@ -32,7 +32,7 @@ import org.springframework.boot.actuate.health.HealthComponent; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.health.HealthEndpointSettings; +import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthStatusHttpMapper; @@ -50,6 +50,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -75,7 +76,7 @@ void runWhenHealthEndpointIsDisabledDoesNotCreateBeans() { this.contextRunner.withPropertyValues("management.endpoint.health.enabled=false").run((context) -> { assertThat(context).doesNotHaveBean(StatusAggregator.class); assertThat(context).doesNotHaveBean(HttpCodeStatusMapper.class); - assertThat(context).doesNotHaveBean(HealthEndpointSettings.class); + assertThat(context).doesNotHaveBean(HealthEndpointGroups.class); assertThat(context).doesNotHaveBean(HealthContributorRegistry.class); assertThat(context).doesNotHaveBean(HealthEndpoint.class); assertThat(context).doesNotHaveBean(ReactiveHealthContributorRegistry.class); @@ -152,19 +153,21 @@ void runWhenHasHttpCodeStatusMapperBeanIgnoresProperties() { } @Test - void runCreatesHealthEndpointSettings() { - this.contextRunner.run((context) -> { - HealthEndpointSettings settings = context.getBean(HealthEndpointSettings.class); - assertThat(settings).isInstanceOf(AutoConfiguredHealthEndpointSettings.class); + void runCreatesHealthEndpointGroups() { + this.contextRunner.withPropertyValues("management.endpoint.health.group.ready.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + assertThat(groups).isInstanceOf(AutoConfiguredHealthEndpointGroups.class); + assertThat(groups.getNames()).containsOnly("ready"); }); } @Test - void runWhenHasHealthEndpointSettingsBeanDoesNotCreateAdditionalHealthEndpointSettings() { - this.contextRunner.withUserConfiguration(HealthEndpointSettingsConfiguration.class).run((context) -> { - HealthEndpointSettings settings = context.getBean(HealthEndpointSettings.class); - assertThat(Mockito.mockingDetails(settings).isMock()).isTrue(); - }); + void runWhenHasHealthEndpointGroupsBeanDoesNotCreateAdditionalHealthEndpointGroups() { + this.contextRunner.withUserConfiguration(HealthEndpointGroupsConfiguration.class) + .withPropertyValues("management.endpoint.health.group.ready.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + assertThat(groups.getNames()).containsOnly("mock"); + }); } @Test @@ -340,11 +343,13 @@ HttpCodeStatusMapper httpCodeStatusMapper() { } @Configuration(proxyBeanMethods = false) - static class HealthEndpointSettingsConfiguration { + static class HealthEndpointGroupsConfiguration { @Bean - HealthEndpointSettings healthEndpointSettings() { - return mock(HealthEndpointSettings.class); + HealthEndpointGroups healthEndpointGroups() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class); + given(groups.getNames()).willReturn(Collections.singleton("mock")); + return groups; } } 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 96a5c76d4903..a8729af2454e 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 @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.health; import java.util.Map; +import java.util.Set; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -43,7 +44,7 @@ public class HealthEndpoint extends HealthEndpointSupport result = getHealth(SecurityContext.NONE, true, path); + return (result != null) ? result.getHealth() : null; } @Override @@ -75,8 +77,8 @@ protected HealthComponent getHealth(HealthContributor contributor, boolean inclu @Override protected HealthComponent aggregateContributions(Map contributions, - StatusAggregator statusAggregator, boolean includeDetails) { - return getCompositeHealth(contributions, statusAggregator, includeDetails); + StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { + return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSettings.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java similarity index 71% rename from spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSettings.java rename to spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java index 75620aadbadc..7fb1816c8918 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSettings.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java @@ -19,12 +19,20 @@ import org.springframework.boot.actuate.endpoint.SecurityContext; /** - * Setting for a {@link HealthEndpoint}. + * A logical grouping of {@link HealthContributor health contributors} that can be exposed + * by the {@link HealthEndpoint}. * * @author Phillip Webb * @since 2.2.0 */ -public interface HealthEndpointSettings { +public interface HealthEndpointGroup { + + /** + * Returns {@code true} if the given contributor is a member of this group. + * @param name the contributor name + * @return {@code true} if the contributor is a member of this group + */ + boolean isMember(String name); /** * Returns if {@link Health#getDetails() health details} should be included in the @@ -35,13 +43,13 @@ public interface HealthEndpointSettings { boolean includeDetails(SecurityContext securityContext); /** - * Returns the status agreggator that should be used for the endpoint. - * @return the status aggregator + * Returns the status agreggator that should be used for this group. + * @return the status aggregator for this group */ StatusAggregator getStatusAggregator(); /** - * Returns the {@link HttpCodeStatusMapper} that should be used for the endpoint. + * Returns the {@link HttpCodeStatusMapper} that should be used for this group. * @return the HTTP code status mapper */ HttpCodeStatusMapper getHttpCodeStatusMapper(); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java new file mode 100644 index 000000000000..1b50c1b4996c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Map; +import java.util.Set; + +import org.springframework.util.Assert; + +/** + * A collection of {@link HealthEndpointGroup groups} for use with a health endpoint. + * + * @author Phillip Webb + * @since 2.2.0 + */ +public interface HealthEndpointGroups { + + /** + * Return the primary group used by the endpoint. + * @return the primary group (never {@code null}) + */ + HealthEndpointGroup getPrimary(); + + /** + * Return the names of any additional groups. + * @return the additional group names + */ + Set getNames(); + + /** + * Return the group with the specified name or {@code null} if the name is not known. + * @param name the name of the group + * @return the {@link HealthEndpointGroup} or {@code null} + */ + HealthEndpointGroup get(String name); + + /** + * Factory method to create a {@link HealthEndpointGroups} instance. + * @param primary the primary group + * @param additional the additional groups + * @return a new {@link HealthEndpointGroups} instance + */ + static HealthEndpointGroups of(HealthEndpointGroup primary, Map additional) { + Assert.notNull(primary, "Primary must not be null"); + Assert.notNull(additional, "Additional must not be null"); + return new HealthEndpointGroups() { + + @Override + public HealthEndpointGroup getPrimary() { + return primary; + } + + @Override + public Set getNames() { + return additional.keySet(); + } + + @Override + public HealthEndpointGroup get(String name) { + return additional.get(name); + } + + }; + } + +} 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 bac86dc5969b..486b04ed33ae 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 @@ -18,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.boot.actuate.endpoint.SecurityContext; @@ -34,7 +35,7 @@ abstract class HealthEndpointSupport { private final ContributorRegistry registry; - private final HealthEndpointSettings settings; + private final HealthEndpointGroups groups; /** * Throw a new {@link IllegalStateException} to indicate a constructor has been @@ -49,37 +50,39 @@ abstract class HealthEndpointSupport { /** * Create a new {@link HealthEndpointSupport} instance. * @param registry the health contributor registry - * @param settings the health settings + * @param groups the health endpoint groups */ - HealthEndpointSupport(ContributorRegistry registry, HealthEndpointSettings settings) { + HealthEndpointSupport(ContributorRegistry registry, HealthEndpointGroups groups) { Assert.notNull(registry, "Registry must not be null"); - Assert.notNull(settings, "Settings must not be null"); + Assert.notNull(groups, "Groups must not be null"); this.registry = registry; - this.settings = settings; + this.groups = groups; } - /** - * Return the health endpoint settings. - * @return the settings - */ - protected final HealthEndpointSettings getSettings() { - return this.settings; + HealthResult getHealth(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { + HealthEndpointGroup group = (path.length > 0) ? this.groups.get(path[0]) : null; + if (group != null) { + return getHealth(group, securityContext, alwaysIncludeDetails, path, 1); + } + return getHealth(this.groups.getPrimary(), securityContext, alwaysIncludeDetails, path, 0); } - T getHealth(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { - boolean includeDetails = alwaysIncludeDetails || this.settings.includeDetails(securityContext); - boolean isRoot = path.length == 0; + private HealthResult getHealth(HealthEndpointGroup group, SecurityContext securityContext, + boolean alwaysIncludeDetails, String[] path, int pathOffset) { + boolean includeDetails = alwaysIncludeDetails || group.includeDetails(securityContext); + boolean isSystemHealth = group == this.groups.getPrimary() && pathOffset == 0; + boolean isRoot = path.length - pathOffset == 0; if (!includeDetails && !isRoot) { return null; } - Object contributor = getContributor(path); - return getContribution(contributor, includeDetails); + Object contributor = getContributor(path, pathOffset); + T health = getContribution(group, contributor, includeDetails, isSystemHealth ? this.groups.getNames() : null); + return (health != null) ? new HealthResult(health, group) : null; } @SuppressWarnings("unchecked") - private Object getContributor(String[] path) { + private Object getContributor(String[] path, int pathOffset) { Object contributor = this.registry; - int pathOffset = 0; while (pathOffset < path.length) { if (!(contributor instanceof NamedContributors)) { return null; @@ -91,37 +94,70 @@ private Object getContributor(String[] path) { } @SuppressWarnings("unchecked") - private T getContribution(Object contributor, boolean includeDetails) { + private T getContribution(HealthEndpointGroup group, Object contributor, boolean includeDetails, + Set groupNames) { if (contributor instanceof NamedContributors) { - return getAggregateHealth((NamedContributors) contributor, includeDetails); + return getAggregateHealth(group, (NamedContributors) contributor, includeDetails, groupNames); } return (contributor != null) ? getHealth((C) contributor, includeDetails) : null; } - private T getAggregateHealth(NamedContributors namedContributors, boolean includeDetails) { + private T getAggregateHealth(HealthEndpointGroup group, NamedContributors namedContributors, + boolean includeDetails, Set groupNames) { Map contributions = new LinkedHashMap<>(); for (NamedContributor namedContributor : namedContributors) { String name = namedContributor.getName(); - T contribution = getContribution(namedContributor.getContributor(), includeDetails); - contributions.put(name, contribution); + if (group.isMember(name)) { + T contribution = getContribution(group, namedContributor.getContributor(), includeDetails, null); + contributions.put(name, contribution); + } } if (contributions.isEmpty()) { return null; } - return aggregateContributions(contributions, this.settings.getStatusAggregator(), includeDetails); + return aggregateContributions(contributions, group.getStatusAggregator(), includeDetails, groupNames); } protected abstract T getHealth(C contributor, boolean includeDetails); protected abstract T aggregateContributions(Map contributions, StatusAggregator statusAggregator, - boolean includeDetails); + boolean includeDetails, Set groupNames); protected final CompositeHealth getCompositeHealth(Map components, - StatusAggregator statusAggregator, boolean includeDetails) { + StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { Status status = statusAggregator.getAggregateStatus( components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet())); Map includedComponents = includeDetails ? components : null; + if (groupNames != null) { + return new SystemHealth(status, includedComponents, groupNames); + } return new CompositeHealth(status, includedComponents); } + /** + * A health result containing health and the group that created it. + * + * @param the contributed health component + */ + static class HealthResult { + + private final T health; + + private final HealthEndpointGroup group; + + HealthResult(T health, HealthEndpointGroup group) { + this.health = health; + this.group = group; + } + + T getHealth() { + return this.health; + } + + HealthEndpointGroup getGroup() { + return this.group; + } + + } + } 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 d05595dcf585..fb6eeaecd59d 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 @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.health; import java.util.Map; +import java.util.Set; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -47,7 +48,7 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport health(SecurityContext securityConte public WebEndpointResponse health(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { - HealthComponent health = getHealth(securityContext, alwaysIncludeDetails, path); - if (health == null) { + HealthResult result = getHealth(securityContext, alwaysIncludeDetails, path); + if (result == null) { return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); } - int statusCode = getSettings().getHttpCodeStatusMapper().getStatusCode(health.getStatus()); + HealthComponent health = result.getHealth(); + HealthEndpointGroup group = result.getGroup(); + int statusCode = group.getHttpCodeStatusMapper().getStatusCode(health.getStatus()); return new WebEndpointResponse<>(health, statusCode); } @@ -90,8 +93,8 @@ protected HealthComponent getHealth(HealthContributor contributor, boolean inclu @Override protected HealthComponent aggregateContributions(Map contributions, - StatusAggregator statusAggregator, boolean includeDetails) { - return getCompositeHealth(contributions, statusAggregator, includeDetails); + StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { + return getCompositeHealth(contributions, statusAggregator, includeDetails, 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 26aa6adac87c..fc8ecf79e824 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 @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.health; import java.util.Map; +import java.util.Set; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -47,7 +48,7 @@ public class ReactiveHealthEndpointWebExtension * @param delegate the delegate health indicator * @param responseMapper the response mapper * @deprecated since 2.2.0 in favor of - * {@link #ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry, HealthEndpointSettings)} + * {@link #ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry, HealthEndpointGroups)} */ @Deprecated public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate, @@ -57,11 +58,10 @@ public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate, /** * Create a new {@link ReactiveHealthEndpointWebExtension} instance. * @param registry the health contributor registry - * @param settings the health endpoint settings + * @param groups the health endpoint groups */ - public ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry registry, - HealthEndpointSettings settings) { - super(registry, settings); + public ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry registry, HealthEndpointGroups groups) { + super(registry, groups); } @ReadOperation @@ -77,12 +77,13 @@ public Mono> health(SecurityConte public Mono> health(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { - Mono result = getHealth(securityContext, alwaysIncludeDetails, path); + HealthResult> result = getHealth(securityContext, alwaysIncludeDetails, path); if (result == null) { return Mono.just(new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND)); } - return result.map((health) -> { - int statusCode = getSettings().getHttpCodeStatusMapper().getStatusCode(health.getStatus()); + HealthEndpointGroup group = result.getGroup(); + return result.getHealth().map((health) -> { + int statusCode = group.getHttpCodeStatusMapper().getStatusCode(health.getStatus()); return new WebEndpointResponse<>(health, statusCode); }); } @@ -95,10 +96,10 @@ protected Mono getHealth(ReactiveHealthContributor co @Override protected Mono aggregateContributions( Map> contributions, StatusAggregator statusAggregator, - boolean includeDetails) { + boolean includeDetails, Set groupNames) { return Flux.fromIterable(contributions.entrySet()).flatMap(NamedHealthComponent::create) .collectMap(NamedHealthComponent::getName, NamedHealthComponent::getHealth) - .map((components) -> this.getCompositeHealth(components, statusAggregator, includeDetails)); + .map((components) -> this.getCompositeHealth(components, statusAggregator, includeDetails, groupNames)); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java new file mode 100644 index 000000000000..bb3acd6e4f9d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * A {@link HealthComponent} that represents the overall system health and the available + * groups. + * + * @author Phillip Webb + * @since 2.2.0 + */ +public final class SystemHealth extends CompositeHealth { + + private final Set groups; + + SystemHealth(Status status, Map instances, Set groups) { + super(status, instances); + this.groups = (groups != null) ? new TreeSet<>(groups) : null; + } + + @JsonInclude(Include.NON_EMPTY) + public Set getGroups() { + return this.groups; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointGroupsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointGroupsTests.java new file mode 100644 index 000000000000..ba176cb31004 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointGroupsTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HealthEndpointGroups}. + * + * @author Phillip Webb + */ +class HealthEndpointGroupsTests { + + @Test + void ofWhenPrimaryIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> HealthEndpointGroups.of(null, Collections.emptyMap())) + .withMessage("Primary must not be null"); + } + + @Test + void ofWhenAdditionalIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> HealthEndpointGroups.of(mock(HealthEndpointGroup.class), null)) + .withMessage("Additional must not be null"); + } + + @Test + void ofReturnsHealthEndpointGroupsInstance() { + HealthEndpointGroup primary = mock(HealthEndpointGroup.class); + HealthEndpointGroup group = mock(HealthEndpointGroup.class); + HealthEndpointGroups groups = HealthEndpointGroups.of(primary, Collections.singletonMap("group", group)); + assertThat(groups.getPrimary()).isSameAs(primary); + assertThat(groups.getNames()).containsExactly("group"); + assertThat(groups.get("group")).isSameAs(group); + assertThat(groups.get("missing")).isNull(); + } + +} 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 4477417cd483..1dd1e25d9072 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 @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.health; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -24,6 +25,7 @@ import org.mockito.MockitoAnnotations; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -44,7 +46,12 @@ abstract class HealthEndpointSupportTests, C, T final Health down = Health.down().build(); - final TestHealthEndpointSettings settings = new TestHealthEndpointSettings(); + final TestHealthEndpointGroup primaryGroup = new TestHealthEndpointGroup(); + + final TestHealthEndpointGroup allTheAs = new TestHealthEndpointGroup((name) -> name.startsWith("a")); + + final HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup, + Collections.singletonMap("alltheas", this.allTheAs)); HealthEndpointSupportTests() { this.registry = createRegistry(); @@ -57,65 +64,76 @@ void setup() { @Test void createWhenRegistryIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> create(null, this.settings)) + assertThatIllegalArgumentException().isThrownBy(() -> create(null, this.groups)) .withMessage("Registry must not be null"); } @Test - void createWhenSettingsIsNullThrowsException() { + void createWhenGroupsIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> create(this.registry, null)) - .withMessage("Settings must not be null"); + .withMessage("Groups must not be null"); } @Test - void getHealthWhenPathIsEmptyReturnsHealth() { + void getHealthResultWhenPathIsEmptyUsesPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); - T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false); + assertThat(result.getGroup()).isEqualTo(this.primaryGroup); assertThat(getHealth(result)).isNotSameAs(this.up); assertThat(getHealth(result).getStatus()).isEqualTo(Status.UP); } @Test - void getHealthWhenHasPathReturnsSubResult() { + void getHealthResultWhenPathIsNotGroupReturnsResultFromPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); - T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false, "test"); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "test"); + assertThat(result.getGroup()).isEqualTo(this.primaryGroup); assertThat(getHealth(result)).isEqualTo(this.up); } @Test - void getHealthWhenAlwaysIncludesDetailsIsFalseAndSettingsIsTrueIncludesDetails() { + void getHealthResultWhenPathIsGroupReturnsResultFromGroup() { + this.registry.registerContributor("atest", createContributor(this.up)); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas", + "atest"); + assertThat(result.getGroup()).isEqualTo(this.allTheAs); + assertThat(getHealth(result)).isEqualTo(this.up); + } + + @Test + void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsTrueIncludesDetails() { this.registry.registerContributor("test", createContributor(this.up)); - T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false, "test"); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "test"); assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot"); } @Test - void getHealthWhenAlwaysIncludesDetailsIsFalseAndSettingsIsFalseIncludesNoDetails() { - this.settings.setIncludeDetails(false); + void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsFalseIncludesNoDetails() { + this.primaryGroup.setIncludeDetails(false); this.registry.registerContributor("test", createContributor(this.up)); - HealthEndpointSupport endpoint = create(this.registry, this.settings); - T rootResult = endpoint.getHealth(SecurityContext.NONE, false); - T componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test"); + HealthEndpointSupport endpoint = create(this.registry, this.groups); + HealthResult rootResult = endpoint.getHealth(SecurityContext.NONE, false); + HealthResult componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test"); assertThat(((CompositeHealth) getHealth(rootResult)).getStatus()).isEqualTo(Status.UP); assertThat(componentResult).isNull(); } @Test - void getHealthWhenAlwaysIncludesDetailsIsTrueIncludesDetails() { - this.settings.setIncludeDetails(false); + void getHealthResultWhenAlwaysIncludesDetailsIsTrueIncludesDetails() { + this.primaryGroup.setIncludeDetails(false); this.registry.registerContributor("test", createContributor(this.up)); - T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, true, "test"); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, true, "test"); assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot"); } @Test - void getHealthWhenCompositeReturnsAggregateResult() { + void getHealthResultWhenCompositeReturnsAggregateResult() { Map contributors = new LinkedHashMap<>(); contributors.put("a", createContributor(this.up)); contributors.put("b", createContributor(this.down)); this.registry.registerContributor("test", createCompositeContributor(contributors)); - T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false); CompositeHealth root = (CompositeHealth) getHealth(result); CompositeHealth component = (CompositeHealth) root.getComponents().get("test"); assertThat(root.getStatus()).isEqualTo(Status.DOWN); @@ -124,12 +142,26 @@ void getHealthWhenCompositeReturnsAggregateResult() { } @Test - void getHealthWhenPathDoesNotExistReturnsNull() { - T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false, "missing"); + void getHealthResultWhenPathDoesNotExistReturnsNull() { + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "missing"); assertThat(result).isNull(); } - protected abstract HealthEndpointSupport create(R registry, HealthEndpointSettings settings); + @Test + void getHealthResultWhenPathIsEmptyIncludesGroups() { + this.registry.registerContributor("test", createContributor(this.up)); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false); + assertThat(((SystemHealth) getHealth(result)).getGroups()).containsOnly("alltheas"); + } + + @Test + void getHealthResultWhenPathIsGroupDoesNotIncludesGroups() { + this.registry.registerContributor("atest", createContributor(this.up)); + HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas"); + assertThat(getHealth(result)).isNotInstanceOf(SystemHealth.class); + } + + protected abstract HealthEndpointSupport create(R registry, HealthEndpointGroups groups); protected abstract R createRegistry(); @@ -137,6 +169,6 @@ void getHealthWhenPathDoesNotExistReturnsNull() { protected abstract C createCompositeContributor(Map contributors); - protected abstract HealthComponent getHealth(T result); + protected abstract HealthComponent getHealth(HealthResult result); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java index 1fb36a29dab4..bf51bdbcd043 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; @@ -42,30 +44,30 @@ void createWhenUsingDeprecatedConstructorThrowsException() { } @Test - void healthReturnsCompositeHealth() { + void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); - HealthComponent health = create(this.registry, this.settings).health(); + HealthComponent health = create(this.registry, this.groups).health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health).isInstanceOf(CompositeHealth.class); + assertThat(health).isInstanceOf(SystemHealth.class); } @Test void healthWhenPathDoesNotExistReturnsNull() { this.registry.registerContributor("test", createContributor(this.up)); - HealthComponent health = create(this.registry, this.settings).healthForPath("missing"); + HealthComponent health = create(this.registry, this.groups).healthForPath("missing"); assertThat(health).isNull(); } @Test void healthWhenPathExistsReturnsHealth() { this.registry.registerContributor("test", createContributor(this.up)); - HealthComponent health = create(this.registry, this.settings).healthForPath("test"); + HealthComponent health = create(this.registry, this.groups).healthForPath("test"); assertThat(health).isEqualTo(this.up); } @Override - protected HealthEndpoint create(HealthContributorRegistry registry, HealthEndpointSettings settings) { - return new HealthEndpoint(registry, settings); + protected HealthEndpoint create(HealthContributorRegistry registry, HealthEndpointGroups groups) { + return new HealthEndpoint(registry, groups); } @Override @@ -84,8 +86,8 @@ protected HealthContributor createCompositeContributor(Map result) { + return result.getHealth(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java index 8a00caab22a8..2e191e626918 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -49,19 +50,18 @@ void createWhenUsingDeprecatedConstructorThrowsException() { @Test void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); - WebEndpointResponse response = create(this.registry, this.settings) - .health(SecurityContext.NONE); + WebEndpointResponse response = create(this.registry, this.groups).health(SecurityContext.NONE); HealthComponent health = response.getBody(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health).isInstanceOf(CompositeHealth.class); + assertThat(health).isInstanceOf(SystemHealth.class); assertThat(response.getStatus()).isEqualTo(200); } @Test void healthWhenPathDoesNotExistReturnsHttp404() { this.registry.registerContributor("test", createContributor(this.up)); - WebEndpointResponse response = create(this.registry, this.settings) - .health(SecurityContext.NONE, "missing"); + WebEndpointResponse response = create(this.registry, this.groups).health(SecurityContext.NONE, + "missing"); assertThat(response.getBody()).isNull(); assertThat(response.getStatus()).isEqualTo(404); } @@ -69,15 +69,15 @@ void healthWhenPathDoesNotExistReturnsHttp404() { @Test void healthWhenPathExistsReturnsHealth() { this.registry.registerContributor("test", createContributor(this.up)); - WebEndpointResponse response = create(this.registry, this.settings) - .health(SecurityContext.NONE, "test"); + WebEndpointResponse response = create(this.registry, this.groups).health(SecurityContext.NONE, + "test"); assertThat(response.getBody()).isEqualTo(this.up); assertThat(response.getStatus()).isEqualTo(200); } @Override - protected HealthEndpointWebExtension create(HealthContributorRegistry registry, HealthEndpointSettings settings) { - return new HealthEndpointWebExtension(registry, settings); + protected HealthEndpointWebExtension create(HealthContributorRegistry registry, HealthEndpointGroups groups) { + return new HealthEndpointWebExtension(registry, groups); } @Override @@ -96,8 +96,8 @@ protected HealthContributor createCompositeContributor(Map result) { + return result.getHealth(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java index 4824ca49126b..c36164d245e9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java @@ -156,28 +156,30 @@ ReactiveHealthContributorRegistry reactiveHealthContributorRegistry( @Bean HealthEndpoint healthEndpoint(HealthContributorRegistry healthContributorRegistry, - HealthEndpointSettings healthEndpointSettings) { - return new HealthEndpoint(healthContributorRegistry, healthEndpointSettings); + HealthEndpointGroups healthEndpointGroups) { + return new HealthEndpoint(healthContributorRegistry, healthEndpointGroups); } @Bean @ConditionalOnWebApplication(type = Type.SERVLET) HealthEndpointWebExtension healthWebEndpointExtension(HealthContributorRegistry healthContributorRegistry, - HealthEndpointSettings healthEndpointSettings) { - return new HealthEndpointWebExtension(healthContributorRegistry, healthEndpointSettings); + HealthEndpointGroups healthEndpointGroups) { + return new HealthEndpointWebExtension(healthContributorRegistry, healthEndpointGroups); } @Bean @ConditionalOnWebApplication(type = Type.REACTIVE) ReactiveHealthEndpointWebExtension reactiveHealthWebEndpointExtension( ReactiveHealthContributorRegistry reactiveHealthContributorRegistry, - HealthEndpointSettings healthEndpointSettings) { - return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, healthEndpointSettings); + HealthEndpointGroups healthEndpointGroups) { + return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, healthEndpointGroups); } @Bean - HealthEndpointSettings healthEndpointSettings() { - return new TestHealthEndpointSettings(); + HealthEndpointGroups healthEndpointGroups() { + TestHealthEndpointGroup primary = new TestHealthEndpointGroup(); + TestHealthEndpointGroup allTheAs = new TestHealthEndpointGroup((name) -> name.startsWith("a")); + return HealthEndpointGroups.of(primary, Collections.singletonMap("alltheas", allTheAs)); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java index be3acef7374a..bf7302ce6fbd 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -50,18 +51,18 @@ void createWhenUsingDeprecatedConstructorThrowsException() { @Test void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); - WebEndpointResponse response = create(this.registry, this.settings) + WebEndpointResponse response = create(this.registry, this.groups) .health(SecurityContext.NONE).block(); HealthComponent health = response.getBody(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health).isInstanceOf(CompositeHealth.class); + assertThat(health).isInstanceOf(SystemHealth.class); assertThat(response.getStatus()).isEqualTo(200); } @Test void healthWhenPathDoesNotExistReturnsHttp404() { this.registry.registerContributor("test", createContributor(this.up)); - WebEndpointResponse response = create(this.registry, this.settings) + WebEndpointResponse response = create(this.registry, this.groups) .health(SecurityContext.NONE, "missing").block(); assertThat(response.getBody()).isNull(); assertThat(response.getStatus()).isEqualTo(404); @@ -70,7 +71,7 @@ void healthWhenPathDoesNotExistReturnsHttp404() { @Test void healthWhenPathExistsReturnsHealth() { this.registry.registerContributor("test", createContributor(this.up)); - WebEndpointResponse response = create(this.registry, this.settings) + WebEndpointResponse response = create(this.registry, this.groups) .health(SecurityContext.NONE, "test").block(); assertThat(response.getBody()).isEqualTo(this.up); assertThat(response.getStatus()).isEqualTo(200); @@ -78,8 +79,8 @@ void healthWhenPathExistsReturnsHealth() { @Override protected ReactiveHealthEndpointWebExtension create(ReactiveHealthContributorRegistry registry, - HealthEndpointSettings settings) { - return new ReactiveHealthEndpointWebExtension(registry, settings); + HealthEndpointGroups groups) { + return new ReactiveHealthEndpointWebExtension(registry, groups); } @Override @@ -99,8 +100,8 @@ protected ReactiveHealthContributor createCompositeContributor( } @Override - protected HealthComponent getHealth(Mono result) { - return result.block(); + protected HealthComponent getHealth(HealthResult> result) { + return result.getHealth().block(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java new file mode 100644 index 000000000000..e8215484ba4f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SystemHealth}. + * + * @author Phillip Webb + */ +class SystemHealthTests { + + @Test + void serializeWithJacksonReturnsValidJson() throws Exception { + Map components = new LinkedHashMap<>(); + components.put("db1", Health.up().build()); + components.put("db2", Health.down().withDetail("a", "b").build()); + Set groups = new LinkedHashSet<>(Arrays.asList("liveness", "readiness")); + CompositeHealth health = new SystemHealth(Status.UP, components, groups); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(health); + assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{" + "\"db1\":{\"status\":\"UP\"}," + + "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}}," + + "\"groups\":[\"liveness\",\"readiness\"]}"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointSettings.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java similarity index 74% rename from spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointSettings.java rename to spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java index aacb1d93024f..cc8c6cab568c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointSettings.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java @@ -16,21 +16,38 @@ package org.springframework.boot.actuate.health; +import java.util.function.Predicate; + import org.springframework.boot.actuate.endpoint.SecurityContext; /** - * Test implementation of {@link HealthEndpointSettings}. + * Test implementation of {@link HealthEndpointGroups}. * * @author Phillip Webb */ -class TestHealthEndpointSettings implements HealthEndpointSettings { +class TestHealthEndpointGroup implements HealthEndpointGroup { private final StatusAggregator statusAggregator = new SimpleStatusAggregator(); private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); + private final Predicate memberPredicate; + private boolean includeDetails = true; + TestHealthEndpointGroup() { + this((name) -> true); + } + + TestHealthEndpointGroup(Predicate memberPredicate) { + this.memberPredicate = memberPredicate; + } + + @Override + public boolean isMember(String name) { + return this.memberPredicate.test(name); + } + @Override public boolean includeDetails(SecurityContext securityContext) { return this.includeDetails; 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 5f555abe211b..47442e1371e0 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 @@ -951,6 +951,42 @@ TIP: If necessary, reactive indicators replace the regular ones. Also, any +==== Health Groups +It's sometimes useful to organize health indicators into groups that can be used for +different purposes. For example, if you deploy your application to Kubernetes, you may +want one different sets of health indicators for your "`liveness`" and "`readiness`" +probes. + +To create a health indicator group you can use the `management.endpoint.health.group.` +property and specify a list of health indicator IDs to `include` or `exclude`. For example, +to create a group that includes only database indicators you can define the following: + +[source,properties,indent=0] +---- + management.endpoint.health.group.custom.include=db +---- + +You can then check the result by hitting `http://localhost:8080/actuator/health/custom`. + +By default groups will inherit the same `StatusAggregator` and `HttpCodeStatusMapper` +settings as the system health, however, these can also be defined on a per-group +basis. It's also possible to override the `show-details` and `roles` properties +if required: + +[source,properties,indent=0] +---- + management.endpoint.health.group.custom.show-details=when-authorized + management.endpoint.health.group.custom.roles=admin + management.endpoint.health.group.custom.status.order=fatal,up + management.endpoint.health.group.custom.status.http-mapping.fatal=500 + +---- + +TIP: You can use `@Qualifier("groupname")` if you need to register custom +`StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group. + + + [[production-ready-application-info]] === Application Information Application information exposes various information collected from all diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 09413b04851a..0abee45e6f15 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -19,3 +19,8 @@ spring.jmx.enabled=true spring.jackson.serialization.write_dates_as_timestamps=false management.trace.http.include=request-headers,response-headers,principal,remote-address,session-id + +management.endpoint.health.show-details=always +management.endpoint.health.group.ready.include=db,diskSpace +management.endpoint.health.group.live.include=example,hello,db +management.endpoint.health.group.live.show-details=never diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java index bc16d4b6295e..ecb4fedfda7b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementPortAndPathSampleActuatorApplicationTests.java @@ -75,7 +75,7 @@ void testHealth() { ResponseEntity entity = new TestRestTemplate().withBasicAuth("user", getPassword()) .getForEntity("http://localhost:" + this.managementPort + "/admin/health", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\"}"); + assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\",\"groups\":[\"live\",\"ready\"]}"); } @Test