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 <snicoll@pivotal.io>
pull/17939/head
Phillip Webb 5 years ago
parent f09e0264d9
commit e03f822c6d

@ -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<String> groupNames;
AutoConfiguredHealthContributorRegistry(Map<String, HealthContributor> contributors,
Collection<String> 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");
}
}

@ -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<String> members;
private final StatusAggregator statusAggregator;
@ -43,20 +45,27 @@ class AutoConfiguredHealthEndpointSettings implements HealthEndpointSettings {
private final Collection<String> 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<String> roles) {
AutoConfiguredHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
HttpCodeStatusMapper httpCodeStatusMapper, ShowDetails showDetails, Collection<String> 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;

@ -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<String> ALL = (name) -> true;
private final HealthEndpointGroup primaryGroup;
private final Map<String, HealthEndpointGroup> 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<String> 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<String, HealthEndpointGroup> createGroups(Map<String, Group> groupProperties, BeanFactory beanFactory,
StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper,
ShowDetails defaultShowDetails, Set<String> defaultRoles) {
Map<String, HealthEndpointGroup> groups = new LinkedHashMap<String, HealthEndpointGroup>();
groupProperties.forEach((groupName, group) -> {
Status status = group.getStatus();
ShowDetails showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails;
Set<String> 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<String> members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude());
groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper,
showDetails, roles));
});
return Collections.unmodifiableMap(groups);
}
private <T> T getNonQualifiedBean(ListableBeanFactory beanFactory, Class<T> type) {
List<String> 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> T getQualifiedBean(BeanFactory beanFactory, Class<T> type, String qualifier, Supplier<T> fallback) {
try {
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, type, qualifier);
}
catch (NoSuchBeanDefinitionException ex) {
return fallback.get();
}
}
@Override
public HealthEndpointGroup getPrimary() {
return this.primaryGroup;
}
@Override
public Set<String> getNames() {
return this.groups.keySet();
}
@Override
public HealthEndpointGroup get(String name) {
return this.groups.get(name);
}
}

@ -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<String> groupNames;
AutoConfiguredReactiveHealthContributorRegistry(Map<String, ReactiveHealthContributor> contributors,
Collection<String> 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");
}
}

@ -18,17 +18,16 @@ package org.springframework.boot.actuate.autoconfigure.health;
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 @@ class HealthEndpointConfiguration {
@Bean
@ConditionalOnMissingBean
HealthEndpointSettings healthEndpointSettings(HealthEndpointProperties properties,
ObjectProvider<StatusAggregator> statusAggregatorProvider,
ObjectProvider<HttpCodeStatusMapper> 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<String, HealthContributor> healthContributors) {
return new DefaultHealthContributorRegistry(healthContributors);
HealthContributorRegistry healthContributorRegistry(Map<String, HealthContributor> 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);
}
}

@ -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 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @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<String> roles = new HashSet<>();
private Map<String, Group> 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<String> getRoles() {
return this.roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
public Map<String, Group> 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<String> order = null;
private Set<String> 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<String, Integer> httpMapping = new HashMap<>();
private Set<String> exclude;
public List<String> getOrder() {
return this.order;
public Set<String> getInclude() {
return this.include;
}
public void setOrder(List<String> statusOrder) {
if (statusOrder != null && !statusOrder.isEmpty()) {
this.order = statusOrder;
}
public void setInclude(Set<String> include) {
this.include = include;
}
public Map<String, Integer> getHttpMapping() {
return this.httpMapping;
public Set<String> 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<String> exclude) {
this.exclude = exclude;
}
}

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

@ -18,7 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.health;
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);
}
}

@ -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<String> 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<String> getRoles() {
return this.roles;
}
public void setRoles(Set<String> 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<String> 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<String, Integer> httpMapping = new HashMap<>();
public List<String> getOrder() {
return this.order;
}
public void setOrder(List<String> statusOrder) {
if (statusOrder != null && !statusOrder.isEmpty()) {
this.order = statusOrder;
}
}
public Map<String, Integer> 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
}
}

@ -21,9 +21,9 @@ import java.util.Map;
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<String, HealthContributor> healthContributors,
Map<String, ReactiveHealthContributor> reactiveHealthContributors) {
Map<String, ReactiveHealthContributor> reactiveHealthContributors, HealthEndpointGroups groups) {
Map<String, ReactiveHealthContributor> 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());
}
}

@ -37,7 +37,7 @@ import org.springframework.boot.actuate.endpoint.web.WebOperation;
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 @@ class CloudFoundryWebEndpointDiscovererTests {
@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

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentatio
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.Health;
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 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
@Bean
HealthEndpoint healthEndpoint(Map<String, HealthContributor> 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 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
}
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;

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

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

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

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

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

@ -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.HealthAggregator;
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.Bean;
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 @@ class HealthEndpointAutoConfigurationTests {
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 @@ class HealthEndpointAutoConfigurationTests {
}
@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 @@ class HealthEndpointAutoConfigurationTests {
}
@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;
}
}

@ -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<HealthContributor, Hea
* healthIndicator} to generate its response.
* @param healthIndicator the health indicator
* @deprecated since 2.2.0 in favor of
* {@link #HealthEndpoint(HealthContributorRegistry, HealthEndpointSettings)}
* {@link #HealthEndpoint(HealthContributorRegistry, HealthEndpointGroups)}
*/
@Deprecated
public HealthEndpoint(HealthIndicator healthIndicator) {
@ -52,10 +53,10 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
/**
* Create a new {@link HealthEndpoint} instance.
* @param registry the health contributor registry
* @param settings the health endpoint settings
* @param groups the health endpoint groups
*/
public HealthEndpoint(HealthContributorRegistry registry, HealthEndpointSettings settings) {
super(registry, settings);
public HealthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups) {
super(registry, groups);
}
@ReadOperation
@ -65,7 +66,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
@ReadOperation
public HealthComponent healthForPath(@Selector(match = Match.ALL_REMAINING) String... path) {
return getHealth(SecurityContext.NONE, true, path);
HealthResult<HealthComponent> result = getHealth(SecurityContext.NONE, true, path);
return (result != null) ? result.getHealth() : null;
}
@Override
@ -75,8 +77,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
@Override
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails) {
return getCompositeHealth(contributions, statusAggregator, includeDetails);
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
}
}

@ -19,12 +19,20 @@ package org.springframework.boot.actuate.health;
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();

@ -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<String> 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<String, HealthEndpointGroup> 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<String> getNames() {
return additional.keySet();
}
@Override
public HealthEndpointGroup get(String name) {
return additional.get(name);
}
};
}
}

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.health;
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<C, T> {
private final ContributorRegistry<C> 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<C, T> {
/**
* 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<C> registry, HealthEndpointSettings settings) {
HealthEndpointSupport(ContributorRegistry<C> 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<T> 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<T> 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<T>(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 @@ abstract class HealthEndpointSupport<C, T> {
}
@SuppressWarnings("unchecked")
private T getContribution(Object contributor, boolean includeDetails) {
private T getContribution(HealthEndpointGroup group, Object contributor, boolean includeDetails,
Set<String> groupNames) {
if (contributor instanceof NamedContributors) {
return getAggregateHealth((NamedContributors<C>) contributor, includeDetails);
return getAggregateHealth(group, (NamedContributors<C>) contributor, includeDetails, groupNames);
}
return (contributor != null) ? getHealth((C) contributor, includeDetails) : null;
}
private T getAggregateHealth(NamedContributors<C> namedContributors, boolean includeDetails) {
private T getAggregateHealth(HealthEndpointGroup group, NamedContributors<C> namedContributors,
boolean includeDetails, Set<String> groupNames) {
Map<String, T> contributions = new LinkedHashMap<>();
for (NamedContributor<C> 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<String, T> contributions, StatusAggregator statusAggregator,
boolean includeDetails);
boolean includeDetails, Set<String> groupNames);
protected final CompositeHealth getCompositeHealth(Map<String, HealthComponent> components,
StatusAggregator statusAggregator, boolean includeDetails) {
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
Status status = statusAggregator.getAggregateStatus(
components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet()));
Map<String, HealthComponent> 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 <T> the contributed health component
*/
static class HealthResult<T> {
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;
}
}
}

@ -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<HealthCont
* @param delegate the delegate endpoint
* @param responseMapper the response mapper
* @deprecated since 2.2.0 in favor of
* {@link #HealthEndpointWebExtension(HealthContributorRegistry, HealthEndpointSettings)}
* {@link #HealthEndpointWebExtension(HealthContributorRegistry, HealthEndpointGroups)}
*/
@Deprecated
public HealthEndpointWebExtension(HealthEndpoint delegate, HealthWebEndpointResponseMapper responseMapper) {
@ -56,10 +57,10 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
/**
* Create a new {@link HealthEndpointWebExtension} instance.
* @param registry the health contributor registry
* @param settings the health endpoint settings
* @param groups the health endpoint groups
*/
public HealthEndpointWebExtension(HealthContributorRegistry registry, HealthEndpointSettings settings) {
super(registry, settings);
public HealthEndpointWebExtension(HealthContributorRegistry registry, HealthEndpointGroups groups) {
super(registry, groups);
}
@ReadOperation
@ -75,11 +76,13 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
public WebEndpointResponse<HealthComponent> health(SecurityContext securityContext, boolean alwaysIncludeDetails,
String... path) {
HealthComponent health = getHealth(securityContext, alwaysIncludeDetails, path);
if (health == null) {
HealthResult<HealthComponent> 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 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
@Override
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails) {
return getCompositeHealth(contributions, statusAggregator, includeDetails);
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
}
}

@ -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 class ReactiveHealthEndpointWebExtension
/**
* 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 class ReactiveHealthEndpointWebExtension
public Mono<WebEndpointResponse<? extends HealthComponent>> health(SecurityContext securityContext,
boolean alwaysIncludeDetails, String... path) {
Mono<? extends HealthComponent> result = getHealth(securityContext, alwaysIncludeDetails, path);
HealthResult<Mono<? extends HealthComponent>> 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 @@ public class ReactiveHealthEndpointWebExtension
@Override
protected Mono<? extends HealthComponent> aggregateContributions(
Map<String, Mono<? extends HealthComponent>> contributions, StatusAggregator statusAggregator,
boolean includeDetails) {
boolean includeDetails, Set<String> 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));
}
/**

@ -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<String> groups;
SystemHealth(Status status, Map<String, HealthComponent> instances, Set<String> groups) {
super(status, instances);
this.groups = (groups != null) ? new TreeSet<>(groups) : null;
}
@JsonInclude(Include.NON_EMPTY)
public Set<String> getGroups() {
return this.groups;
}
}

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

@ -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.junit.jupiter.api.Test;
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<R extends ContributorRegistry<C>, 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 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@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<T> 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<T> 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<T> 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<T> 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<C, T> endpoint = create(this.registry, this.settings);
T rootResult = endpoint.getHealth(SecurityContext.NONE, false);
T componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test");
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(SecurityContext.NONE, false);
HealthResult<T> 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<T> 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<String, C> 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<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false);
CompositeHealth root = (CompositeHealth) getHealth(result);
CompositeHealth component = (CompositeHealth) root.getDetails().get("test");
assertThat(root.getStatus()).isEqualTo(Status.DOWN);
@ -124,12 +142,26 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
}
@Test
void getHealthWhenPathDoesNotExistReturnsNull() {
T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false, "missing");
void getHealthResultWhenPathDoesNotExistReturnsNull() {
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "missing");
assertThat(result).isNull();
}
protected abstract HealthEndpointSupport<C, T> create(R registry, HealthEndpointSettings settings);
@Test
void getHealthResultWhenPathIsEmptyIncludesGroups() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> 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<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas");
assertThat(getHealth(result)).isNotInstanceOf(SystemHealth.class);
}
protected abstract HealthEndpointSupport<C, T> create(R registry, HealthEndpointGroups groups);
protected abstract R createRegistry();
@ -137,6 +169,6 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
protected abstract C createCompositeContributor(Map<String, C> contributors);
protected abstract HealthComponent getHealth(T result);
protected abstract HealthComponent getHealth(HealthResult<T> result);
}

@ -20,6 +20,8 @@ import java.util.Map;
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 @@ class HealthEndpointTests
}
@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 @@ class HealthEndpointTests
}
@Override
protected HealthComponent getHealth(HealthComponent result) {
return result;
protected HealthComponent getHealth(HealthResult<HealthComponent> result) {
return result.getHealth();
}
}

@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
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 @@ class HealthEndpointWebExtensionTests
@Test
void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.settings)
.health(SecurityContext.NONE);
WebEndpointResponse<HealthComponent> 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<HealthComponent> response = create(this.registry, this.settings)
.health(SecurityContext.NONE, "missing");
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE,
"missing");
assertThat(response.getBody()).isNull();
assertThat(response.getStatus()).isEqualTo(404);
}
@ -69,15 +69,15 @@ class HealthEndpointWebExtensionTests
@Test
void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.settings)
.health(SecurityContext.NONE, "test");
WebEndpointResponse<HealthComponent> 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 @@ class HealthEndpointWebExtensionTests
}
@Override
protected HealthComponent getHealth(HealthComponent result) {
return result;
protected HealthComponent getHealth(HealthResult<HealthComponent> result) {
return result.getHealth();
}
}

@ -156,28 +156,30 @@ class HealthEndpointWebIntegrationTests {
@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

@ -23,6 +23,7 @@ import reactor.core.publisher.Mono;
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 @@ class ReactiveHealthEndpointWebExtensionTests extends
@Test
void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.settings)
WebEndpointResponse<? extends HealthComponent> 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<? extends HealthComponent> response = create(this.registry, this.settings)
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
.health(SecurityContext.NONE, "missing").block();
assertThat(response.getBody()).isNull();
assertThat(response.getStatus()).isEqualTo(404);
@ -70,7 +71,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
@Test
void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.settings)
WebEndpointResponse<? extends HealthComponent> 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 @@ class ReactiveHealthEndpointWebExtensionTests extends
@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 @@ class ReactiveHealthEndpointWebExtensionTests extends
}
@Override
protected HealthComponent getHealth(Mono<? extends HealthComponent> result) {
return result.block();
protected HealthComponent getHealth(HealthResult<Mono<? extends HealthComponent>> result) {
return result.getHealth().block();
}
}

@ -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<String, HealthComponent> components = new LinkedHashMap<>();
components.put("db1", Health.up().build());
components.put("db2", Health.down().withDetail("a", "b").build());
Set<String> 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\",\"details\":{" + "\"db1\":{\"status\":\"UP\"},"
+ "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}},"
+ "\"groups\":[\"liveness\",\"readiness\"]}");
}
}

@ -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<String> memberPredicate;
private boolean includeDetails = true;
TestHealthEndpointGroup() {
this((name) -> true);
}
TestHealthEndpointGroup(Predicate<String> memberPredicate) {
this.memberPredicate = memberPredicate;
}
@Override
public boolean isMember(String name) {
return this.memberPredicate.test(name);
}
@Override
public boolean includeDetails(SecurityContext securityContext) {
return this.includeDetails;

@ -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.<name>`
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

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

@ -75,7 +75,7 @@ class ManagementPortAndPathSampleActuatorApplicationTests {
ResponseEntity<String> 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

Loading…
Cancel
Save