Commit e03f822c authored by Phillip Webb's avatar Phillip Webb

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: 's avatarStephane Nicoll <snicoll@pivotal.io>
parent f09e0264
/*
* 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 @@ ...@@ -17,22 +17,24 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collection; 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.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.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
/** /**
* Auto-configured {@link HealthEndpointSettings} backed by * Auto-configured {@link HealthEndpointGroup} backed by {@link HealthProperties}.
* {@link HealthEndpointProperties}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class AutoConfiguredHealthEndpointSettings implements HealthEndpointSettings { class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
private final Predicate<String> members;
private final StatusAggregator statusAggregator; private final StatusAggregator statusAggregator;
...@@ -43,20 +45,27 @@ class AutoConfiguredHealthEndpointSettings implements HealthEndpointSettings { ...@@ -43,20 +45,27 @@ class AutoConfiguredHealthEndpointSettings implements HealthEndpointSettings {
private final Collection<String> roles; 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 statusAggregator the status aggregator to use
* @param httpCodeStatusMapper the HTTP code status mapper to use * @param httpCodeStatusMapper the HTTP code status mapper to use
* @param showDetails the show details setting * @param showDetails the show details setting
* @param roles the roles to match * @param roles the roles to match
*/ */
AutoConfiguredHealthEndpointSettings(StatusAggregator statusAggregator, HttpCodeStatusMapper httpCodeStatusMapper, AutoConfiguredHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
ShowDetails showDetails, Collection<String> roles) { HttpCodeStatusMapper httpCodeStatusMapper, ShowDetails showDetails, Collection<String> roles) {
this.members = members;
this.statusAggregator = statusAggregator; this.statusAggregator = statusAggregator;
this.httpCodeStatusMapper = httpCodeStatusMapper; this.httpCodeStatusMapper = httpCodeStatusMapper;
this.showDetails = showDetails; this.showDetails = showDetails;
this.roles = roles; this.roles = roles;
} }
@Override
public boolean isMember(String name) {
return this.members.test(name);
}
@Override @Override
public boolean includeDetails(SecurityContext securityContext) { public boolean includeDetails(SecurityContext securityContext) {
ShowDetails showDetails = this.showDetails; ShowDetails showDetails = this.showDetails;
......
/*
* 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);
}
}
/*
* 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; ...@@ -18,17 +18,16 @@ package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Map; 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.HealthContributor;
import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
import org.springframework.boot.actuate.health.SimpleStatusAggregator; import org.springframework.boot.actuate.health.SimpleStatusAggregator;
import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -55,27 +54,22 @@ class HealthEndpointConfiguration { ...@@ -55,27 +54,22 @@ class HealthEndpointConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
HealthEndpointSettings healthEndpointSettings(HealthEndpointProperties properties, HealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext,
ObjectProvider<StatusAggregator> statusAggregatorProvider, HealthEndpointProperties properties) {
ObjectProvider<HttpCodeStatusMapper> httpCodeStatusMapperProvider) { return new AutoConfiguredHealthEndpointGroups(applicationContext, properties);
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());
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
HealthContributorRegistry healthContributorRegistry(Map<String, HealthContributor> healthContributors) { HealthContributorRegistry healthContributorRegistry(Map<String, HealthContributor> healthContributors,
return new DefaultHealthContributorRegistry(healthContributors); HealthEndpointGroups groups) {
return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames());
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointSettings settings) { HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups) {
return new HealthEndpoint(registry, settings); return new HealthEndpoint(registry, groups);
} }
} }
...@@ -16,9 +16,7 @@ ...@@ -16,9 +16,7 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.util.HashMap; import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
...@@ -32,93 +30,47 @@ import org.springframework.boot.context.properties.ConfigurationProperties; ...@@ -32,93 +30,47 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @since 2.0.0 * @since 2.0.0
*/ */
@ConfigurationProperties("management.endpoint.health") @ConfigurationProperties("management.endpoint.health")
public class HealthEndpointProperties { public class HealthEndpointProperties extends 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. * Health endpoint groups.
* When empty, all authenticated users are authorized.
*/ */
private Set<String> roles = new HashSet<>(); private Map<String, Group> group = new LinkedHashMap<>();
public Status getStatus() { public Map<String, Group> getGroup() {
return this.status; return this.group;
}
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. * 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 * The health indicator IDs to exclude. Use '*' if you want to exclude all.
* statuses map to sensible defaults (for example, UP maps to 200).
*/ */
private final Map<String, Integer> httpMapping = new HashMap<>(); private Set<String> exclude;
public List<String> getOrder() { public Set<String> getInclude() {
return this.order; return this.include;
} }
public void setOrder(List<String> statusOrder) { public void setInclude(Set<String> include) {
if (statusOrder != null && !statusOrder.isEmpty()) { this.include = include;
this.order = statusOrder;
}
} }
public Map<String, Integer> getHttpMapping() { public Set<String> getExclude() {
return this.httpMapping; return this.exclude;
} }
} public void setExclude(Set<String> exclude) {
this.exclude = 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
} }
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.ReactiveHealthContributorRegistry;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
...@@ -42,8 +42,8 @@ class HealthEndpointReactiveWebExtensionConfiguration { ...@@ -42,8 +42,8 @@ class HealthEndpointReactiveWebExtensionConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnBean(HealthEndpoint.class) @ConditionalOnBean(HealthEndpoint.class)
ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension( ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension(
ReactiveHealthContributorRegistry reactiveHealthContributorRegistry, HealthEndpointSettings settings) { ReactiveHealthContributorRegistry reactiveHealthContributorRegistry, HealthEndpointGroups groups) {
return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, settings); return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, groups);
} }
} }
...@@ -18,7 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.health; ...@@ -18,7 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.health;
import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.HealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
...@@ -42,8 +42,8 @@ class HealthEndpointWebExtensionConfiguration { ...@@ -42,8 +42,8 @@ class HealthEndpointWebExtensionConfiguration {
@ConditionalOnBean(HealthEndpoint.class) @ConditionalOnBean(HealthEndpoint.class)
@ConditionalOnMissingBean @ConditionalOnMissingBean
HealthEndpointWebExtension healthEndpointWebExtension(HealthContributorRegistry healthContributorRegistry, HealthEndpointWebExtension healthEndpointWebExtension(HealthContributorRegistry healthContributorRegistry,
HealthEndpointSettings settings) { HealthEndpointGroups groups) {
return new HealthEndpointWebExtension(healthContributorRegistry, settings); return new HealthEndpointWebExtension(healthContributorRegistry, groups);
} }
} }
/*
* 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; ...@@ -21,9 +21,9 @@ import java.util.Map;
import reactor.core.publisher.Flux; 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.HealthContributor;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.ReactiveHealthContributor;
import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
...@@ -47,11 +47,11 @@ class ReactiveHealthEndpointConfiguration { ...@@ -47,11 +47,11 @@ class ReactiveHealthEndpointConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
ReactiveHealthContributorRegistry reactiveHealthContributorRegistry( ReactiveHealthContributorRegistry reactiveHealthContributorRegistry(
Map<String, HealthContributor> healthContributors, Map<String, HealthContributor> healthContributors,
Map<String, ReactiveHealthContributor> reactiveHealthContributors) { Map<String, ReactiveHealthContributor> reactiveHealthContributors, HealthEndpointGroups groups) {
Map<String, ReactiveHealthContributor> allContributors = new LinkedHashMap<>(reactiveHealthContributors); Map<String, ReactiveHealthContributor> allContributors = new LinkedHashMap<>(reactiveHealthContributors);
healthContributors.forEach((name, contributor) -> allContributors.computeIfAbsent(name, healthContributors.forEach((name, contributor) -> allContributors.computeIfAbsent(name,
(key) -> ReactiveHealthContributor.adapt(contributor))); (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; ...@@ -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.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -112,8 +112,8 @@ class CloudFoundryWebEndpointDiscovererTests { ...@@ -112,8 +112,8 @@ class CloudFoundryWebEndpointDiscovererTests {
@Bean @Bean
HealthEndpoint healthEndpoint() { HealthEndpoint healthEndpoint() {
HealthContributorRegistry registry = mock(HealthContributorRegistry.class); HealthContributorRegistry registry = mock(HealthContributorRegistry.class);
HealthEndpointSettings settings = mock(HealthEndpointSettings.class); HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
return new HealthEndpoint(registry, settings); return new HealthEndpoint(registry, groups);
} }
@Bean @Bean
......
...@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentatio ...@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentatio
import java.io.File; import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -33,7 +34,8 @@ import org.springframework.boot.actuate.health.Health; ...@@ -33,7 +34,8 @@ import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.HealthIndicator;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper; import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
...@@ -104,8 +106,9 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests ...@@ -104,8 +106,9 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
@Bean @Bean
HealthEndpoint healthEndpoint(Map<String, HealthContributor> healthContributors) { HealthEndpoint healthEndpoint(Map<String, HealthContributor> healthContributors) {
HealthContributorRegistry registry = new DefaultHealthContributorRegistry(healthContributors); HealthContributorRegistry registry = new DefaultHealthContributorRegistry(healthContributors);
HealthEndpointSettings settings = new TestHealthEndpointSettings(); HealthEndpointGroup primary = new TestHealthEndpointGroup();
return new HealthEndpoint(registry, settings); HealthEndpointGroups groups = HealthEndpointGroups.of(primary, Collections.emptyMap());
return new HealthEndpoint(registry, groups);
} }
@Bean @Bean
...@@ -128,12 +131,17 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests ...@@ -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 StatusAggregator statusAggregator = new SimpleStatusAggregator();
private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper();
@Override
public boolean isMember(String name) {
return true;
}
@Override @Override
public boolean includeDetails(SecurityContext securityContext) { public boolean includeDetails(SecurityContext securityContext) {
return true; return true;
......
/*
* 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");
}
}
...@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; ...@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
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.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.actuate.health.StatusAggregator;
...@@ -34,11 +34,11 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -34,11 +34,11 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
/** /**
* Tests for {@link AutoConfiguredHealthEndpointSettings}. * Tests for {@link AutoConfiguredHealthEndpointGroup}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
class AutoConfiguredHealthEndpointSettingsTests { class AutoConfiguredHealthEndpointGroupTests {
@Mock @Mock
private StatusAggregator statusAggregator; private StatusAggregator statusAggregator;
...@@ -57,66 +57,84 @@ class AutoConfiguredHealthEndpointSettingsTests { ...@@ -57,66 +57,84 @@ class AutoConfiguredHealthEndpointSettingsTests {
MockitoAnnotations.initMocks(this); 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 @Test
void includeDetailsWhenShowDetailsIsNeverReturnsFalse() { void includeDetailsWhenShowDetailsIsNeverReturnsFalse() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.NEVER, Collections.emptySet()); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.NEVER, Collections.emptySet());
assertThat(settings.includeDetails(SecurityContext.NONE)).isFalse(); assertThat(group.includeDetails(SecurityContext.NONE)).isFalse();
} }
@Test @Test
void includeDetailsWhenShowDetailsIsAlwaysReturnsTrue() { void includeDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
assertThat(settings.includeDetails(SecurityContext.NONE)).isTrue(); assertThat(group.includeDetails(SecurityContext.NONE)).isTrue();
} }
@Test @Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() { void includeDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet()); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(null); given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(settings.includeDetails(this.securityContext)).isFalse(); assertThat(group.includeDetails(this.securityContext)).isFalse();
} }
@Test @Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() { void includeDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet()); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(this.principal); given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(settings.includeDetails(this.securityContext)).isTrue(); assertThat(group.includeDetails(this.securityContext)).isTrue();
} }
@Test @Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Arrays.asList("admin", "root", "bossmode")); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal); given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true); given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(settings.includeDetails(this.securityContext)).isTrue(); assertThat(group.includeDetails(this.securityContext)).isTrue();
} }
@Test @Test
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() { void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Arrays.asList("admin", "rot", "bossmode")); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED,
Arrays.asList("admin", "rot", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal); given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true); given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(settings.includeDetails(this.securityContext)).isFalse(); assertThat(group.includeDetails(this.securityContext)).isFalse();
} }
@Test @Test
void getStatusAggregatorReturnsStatusAggregator() { void getStatusAggregatorReturnsStatusAggregator() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
assertThat(settings.getStatusAggregator()).isSameAs(this.statusAggregator); assertThat(group.getStatusAggregator()).isSameAs(this.statusAggregator);
} }
@Test @Test
void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() { void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() {
AutoConfiguredHealthEndpointSettings settings = new AutoConfiguredHealthEndpointSettings(this.statusAggregator, AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet()); this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
assertThat(settings.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper); assertThat(group.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper);
} }
} }
/*
* 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));
}
}
}
/*
* 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 @@ ...@@ -16,10 +16,10 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
...@@ -32,7 +32,7 @@ import org.springframework.boot.actuate.health.HealthAggregator; ...@@ -32,7 +32,7 @@ import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthComponent; import org.springframework.boot.actuate.health.HealthComponent;
import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint; 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.HealthEndpointWebExtension;
import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper; import org.springframework.boot.actuate.health.HealthStatusHttpMapper;
...@@ -50,6 +50,7 @@ import org.springframework.context.annotation.Bean; ...@@ -50,6 +50,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
...@@ -75,7 +76,7 @@ class HealthEndpointAutoConfigurationTests { ...@@ -75,7 +76,7 @@ class HealthEndpointAutoConfigurationTests {
this.contextRunner.withPropertyValues("management.endpoint.health.enabled=false").run((context) -> { this.contextRunner.withPropertyValues("management.endpoint.health.enabled=false").run((context) -> {
assertThat(context).doesNotHaveBean(StatusAggregator.class); assertThat(context).doesNotHaveBean(StatusAggregator.class);
assertThat(context).doesNotHaveBean(HttpCodeStatusMapper.class); assertThat(context).doesNotHaveBean(HttpCodeStatusMapper.class);
assertThat(context).doesNotHaveBean(HealthEndpointSettings.class); assertThat(context).doesNotHaveBean(HealthEndpointGroups.class);
assertThat(context).doesNotHaveBean(HealthContributorRegistry.class); assertThat(context).doesNotHaveBean(HealthContributorRegistry.class);
assertThat(context).doesNotHaveBean(HealthEndpoint.class); assertThat(context).doesNotHaveBean(HealthEndpoint.class);
assertThat(context).doesNotHaveBean(ReactiveHealthContributorRegistry.class); assertThat(context).doesNotHaveBean(ReactiveHealthContributorRegistry.class);
...@@ -152,19 +153,21 @@ class HealthEndpointAutoConfigurationTests { ...@@ -152,19 +153,21 @@ class HealthEndpointAutoConfigurationTests {
} }
@Test @Test
void runCreatesHealthEndpointSettings() { void runCreatesHealthEndpointGroups() {
this.contextRunner.run((context) -> { this.contextRunner.withPropertyValues("management.endpoint.health.group.ready.include=*").run((context) -> {
HealthEndpointSettings settings = context.getBean(HealthEndpointSettings.class); HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class);
assertThat(settings).isInstanceOf(AutoConfiguredHealthEndpointSettings.class); assertThat(groups).isInstanceOf(AutoConfiguredHealthEndpointGroups.class);
assertThat(groups.getNames()).containsOnly("ready");
}); });
} }
@Test @Test
void runWhenHasHealthEndpointSettingsBeanDoesNotCreateAdditionalHealthEndpointSettings() { void runWhenHasHealthEndpointGroupsBeanDoesNotCreateAdditionalHealthEndpointGroups() {
this.contextRunner.withUserConfiguration(HealthEndpointSettingsConfiguration.class).run((context) -> { this.contextRunner.withUserConfiguration(HealthEndpointGroupsConfiguration.class)
HealthEndpointSettings settings = context.getBean(HealthEndpointSettings.class); .withPropertyValues("management.endpoint.health.group.ready.include=*").run((context) -> {
assertThat(Mockito.mockingDetails(settings).isMock()).isTrue(); HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class);
}); assertThat(groups.getNames()).containsOnly("mock");
});
} }
@Test @Test
...@@ -340,11 +343,13 @@ class HealthEndpointAutoConfigurationTests { ...@@ -340,11 +343,13 @@ class HealthEndpointAutoConfigurationTests {
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
static class HealthEndpointSettingsConfiguration { static class HealthEndpointGroupsConfiguration {
@Bean @Bean
HealthEndpointSettings healthEndpointSettings() { HealthEndpointGroups healthEndpointGroups() {
return mock(HealthEndpointSettings.class); HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
given(groups.getNames()).willReturn(Collections.singleton("mock"));
return groups;
} }
} }
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
...@@ -43,7 +44,7 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea ...@@ -43,7 +44,7 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
* healthIndicator} to generate its response. * healthIndicator} to generate its response.
* @param healthIndicator the health indicator * @param healthIndicator the health indicator
* @deprecated since 2.2.0 in favor of * @deprecated since 2.2.0 in favor of
* {@link #HealthEndpoint(HealthContributorRegistry, HealthEndpointSettings)} * {@link #HealthEndpoint(HealthContributorRegistry, HealthEndpointGroups)}
*/ */
@Deprecated @Deprecated
public HealthEndpoint(HealthIndicator healthIndicator) { public HealthEndpoint(HealthIndicator healthIndicator) {
...@@ -52,10 +53,10 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea ...@@ -52,10 +53,10 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
/** /**
* Create a new {@link HealthEndpoint} instance. * Create a new {@link HealthEndpoint} instance.
* @param registry the health contributor registry * @param registry the health contributor registry
* @param settings the health endpoint settings * @param groups the health endpoint groups
*/ */
public HealthEndpoint(HealthContributorRegistry registry, HealthEndpointSettings settings) { public HealthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups groups) {
super(registry, settings); super(registry, groups);
} }
@ReadOperation @ReadOperation
...@@ -65,7 +66,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea ...@@ -65,7 +66,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
@ReadOperation @ReadOperation
public HealthComponent healthForPath(@Selector(match = Match.ALL_REMAINING) String... path) { 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 @Override
...@@ -75,8 +77,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea ...@@ -75,8 +77,8 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
@Override @Override
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions, protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails) { StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(contributions, statusAggregator, includeDetails); return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
} }
} }
...@@ -19,12 +19,20 @@ package org.springframework.boot.actuate.health; ...@@ -19,12 +19,20 @@ package org.springframework.boot.actuate.health;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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 * @author Phillip Webb
* @since 2.2.0 * @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 * Returns if {@link Health#getDetails() health details} should be included in the
...@@ -35,13 +43,13 @@ public interface HealthEndpointSettings { ...@@ -35,13 +43,13 @@ public interface HealthEndpointSettings {
boolean includeDetails(SecurityContext securityContext); boolean includeDetails(SecurityContext securityContext);
/** /**
* Returns the status agreggator that should be used for the endpoint. * Returns the status agreggator that should be used for this group.
* @return the status aggregator * @return the status aggregator for this group
*/ */
StatusAggregator getStatusAggregator(); 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 * @return the HTTP code status mapper
*/ */
HttpCodeStatusMapper getHttpCodeStatusMapper(); HttpCodeStatusMapper getHttpCodeStatusMapper();
......
/*
* 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; ...@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.health;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
...@@ -34,7 +35,7 @@ abstract class HealthEndpointSupport<C, T> { ...@@ -34,7 +35,7 @@ abstract class HealthEndpointSupport<C, T> {
private final ContributorRegistry<C> registry; private final ContributorRegistry<C> registry;
private final HealthEndpointSettings settings; private final HealthEndpointGroups groups;
/** /**
* Throw a new {@link IllegalStateException} to indicate a constructor has been * Throw a new {@link IllegalStateException} to indicate a constructor has been
...@@ -49,37 +50,39 @@ abstract class HealthEndpointSupport<C, T> { ...@@ -49,37 +50,39 @@ abstract class HealthEndpointSupport<C, T> {
/** /**
* Create a new {@link HealthEndpointSupport} instance. * Create a new {@link HealthEndpointSupport} instance.
* @param registry the health contributor registry * @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(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.registry = registry;
this.settings = settings; this.groups = groups;
} }
/** HealthResult<T> getHealth(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) {
* Return the health endpoint settings. HealthEndpointGroup group = (path.length > 0) ? this.groups.get(path[0]) : null;
* @return the settings if (group != null) {
*/ return getHealth(group, securityContext, alwaysIncludeDetails, path, 1);
protected final HealthEndpointSettings getSettings() { }
return this.settings; return getHealth(this.groups.getPrimary(), securityContext, alwaysIncludeDetails, path, 0);
} }
T getHealth(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { private HealthResult<T> getHealth(HealthEndpointGroup group, SecurityContext securityContext,
boolean includeDetails = alwaysIncludeDetails || this.settings.includeDetails(securityContext); boolean alwaysIncludeDetails, String[] path, int pathOffset) {
boolean isRoot = path.length == 0; boolean includeDetails = alwaysIncludeDetails || group.includeDetails(securityContext);
boolean isSystemHealth = group == this.groups.getPrimary() && pathOffset == 0;
boolean isRoot = path.length - pathOffset == 0;
if (!includeDetails && !isRoot) { if (!includeDetails && !isRoot) {
return null; return null;
} }
Object contributor = getContributor(path); Object contributor = getContributor(path, pathOffset);
return getContribution(contributor, includeDetails); T health = getContribution(group, contributor, includeDetails, isSystemHealth ? this.groups.getNames() : null);
return (health != null) ? new HealthResult<T>(health, group) : null;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Object getContributor(String[] path) { private Object getContributor(String[] path, int pathOffset) {
Object contributor = this.registry; Object contributor = this.registry;
int pathOffset = 0;
while (pathOffset < path.length) { while (pathOffset < path.length) {
if (!(contributor instanceof NamedContributors)) { if (!(contributor instanceof NamedContributors)) {
return null; return null;
...@@ -91,37 +94,70 @@ abstract class HealthEndpointSupport<C, T> { ...@@ -91,37 +94,70 @@ abstract class HealthEndpointSupport<C, T> {
} }
@SuppressWarnings("unchecked") @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) { 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; 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<>(); Map<String, T> contributions = new LinkedHashMap<>();
for (NamedContributor<C> namedContributor : namedContributors) { for (NamedContributor<C> namedContributor : namedContributors) {
String name = namedContributor.getName(); String name = namedContributor.getName();
T contribution = getContribution(namedContributor.getContributor(), includeDetails); if (group.isMember(name)) {
contributions.put(name, contribution); T contribution = getContribution(group, namedContributor.getContributor(), includeDetails, null);
contributions.put(name, contribution);
}
} }
if (contributions.isEmpty()) { if (contributions.isEmpty()) {
return null; 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 getHealth(C contributor, boolean includeDetails);
protected abstract T aggregateContributions(Map<String, T> contributions, StatusAggregator statusAggregator, 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, protected final CompositeHealth getCompositeHealth(Map<String, HealthComponent> components,
StatusAggregator statusAggregator, boolean includeDetails) { StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
Status status = statusAggregator.getAggregateStatus( Status status = statusAggregator.getAggregateStatus(
components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet())); components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet()));
Map<String, HealthComponent> includedComponents = includeDetails ? components : null; Map<String, HealthComponent> includedComponents = includeDetails ? components : null;
if (groupNames != null) {
return new SystemHealth(status, includedComponents, groupNames);
}
return new CompositeHealth(status, includedComponents); 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 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
...@@ -47,7 +48,7 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont ...@@ -47,7 +48,7 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
* @param delegate the delegate endpoint * @param delegate the delegate endpoint
* @param responseMapper the response mapper * @param responseMapper the response mapper
* @deprecated since 2.2.0 in favor of * @deprecated since 2.2.0 in favor of
* {@link #HealthEndpointWebExtension(HealthContributorRegistry, HealthEndpointSettings)} * {@link #HealthEndpointWebExtension(HealthContributorRegistry, HealthEndpointGroups)}
*/ */
@Deprecated @Deprecated
public HealthEndpointWebExtension(HealthEndpoint delegate, HealthWebEndpointResponseMapper responseMapper) { public HealthEndpointWebExtension(HealthEndpoint delegate, HealthWebEndpointResponseMapper responseMapper) {
...@@ -56,10 +57,10 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont ...@@ -56,10 +57,10 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
/** /**
* Create a new {@link HealthEndpointWebExtension} instance. * Create a new {@link HealthEndpointWebExtension} instance.
* @param registry the health contributor registry * @param registry the health contributor registry
* @param settings the health endpoint settings * @param groups the health endpoint groups
*/ */
public HealthEndpointWebExtension(HealthContributorRegistry registry, HealthEndpointSettings settings) { public HealthEndpointWebExtension(HealthContributorRegistry registry, HealthEndpointGroups groups) {
super(registry, settings); super(registry, groups);
} }
@ReadOperation @ReadOperation
...@@ -75,11 +76,13 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont ...@@ -75,11 +76,13 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
public WebEndpointResponse<HealthComponent> health(SecurityContext securityContext, boolean alwaysIncludeDetails, public WebEndpointResponse<HealthComponent> health(SecurityContext securityContext, boolean alwaysIncludeDetails,
String... path) { String... path) {
HealthComponent health = getHealth(securityContext, alwaysIncludeDetails, path); HealthResult<HealthComponent> result = getHealth(securityContext, alwaysIncludeDetails, path);
if (health == null) { if (result == null) {
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); 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); return new WebEndpointResponse<>(health, statusCode);
} }
...@@ -90,8 +93,8 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont ...@@ -90,8 +93,8 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
@Override @Override
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions, protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails) { StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(contributions, statusAggregator, includeDetails); return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
} }
} }
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.Map; import java.util.Map;
import java.util.Set;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
...@@ -47,7 +48,7 @@ public class ReactiveHealthEndpointWebExtension ...@@ -47,7 +48,7 @@ public class ReactiveHealthEndpointWebExtension
* @param delegate the delegate health indicator * @param delegate the delegate health indicator
* @param responseMapper the response mapper * @param responseMapper the response mapper
* @deprecated since 2.2.0 in favor of * @deprecated since 2.2.0 in favor of
* {@link #ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry, HealthEndpointSettings)} * {@link #ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry, HealthEndpointGroups)}
*/ */
@Deprecated @Deprecated
public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate, public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate,
...@@ -57,11 +58,10 @@ public class ReactiveHealthEndpointWebExtension ...@@ -57,11 +58,10 @@ public class ReactiveHealthEndpointWebExtension
/** /**
* Create a new {@link ReactiveHealthEndpointWebExtension} instance. * Create a new {@link ReactiveHealthEndpointWebExtension} instance.
* @param registry the health contributor registry * @param registry the health contributor registry
* @param settings the health endpoint settings * @param groups the health endpoint groups
*/ */
public ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry registry, public ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry registry, HealthEndpointGroups groups) {
HealthEndpointSettings settings) { super(registry, groups);
super(registry, settings);
} }
@ReadOperation @ReadOperation
...@@ -77,12 +77,13 @@ public class ReactiveHealthEndpointWebExtension ...@@ -77,12 +77,13 @@ public class ReactiveHealthEndpointWebExtension
public Mono<WebEndpointResponse<? extends HealthComponent>> health(SecurityContext securityContext, public Mono<WebEndpointResponse<? extends HealthComponent>> health(SecurityContext securityContext,
boolean alwaysIncludeDetails, String... path) { boolean alwaysIncludeDetails, String... path) {
Mono<? extends HealthComponent> result = getHealth(securityContext, alwaysIncludeDetails, path); HealthResult<Mono<? extends HealthComponent>> result = getHealth(securityContext, alwaysIncludeDetails, path);
if (result == null) { if (result == null) {
return Mono.just(new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND)); return Mono.just(new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND));
} }
return result.map((health) -> { HealthEndpointGroup group = result.getGroup();
int statusCode = getSettings().getHttpCodeStatusMapper().getStatusCode(health.getStatus()); return result.getHealth().map((health) -> {
int statusCode = group.getHttpCodeStatusMapper().getStatusCode(health.getStatus());
return new WebEndpointResponse<>(health, statusCode); return new WebEndpointResponse<>(health, statusCode);
}); });
} }
...@@ -95,10 +96,10 @@ public class ReactiveHealthEndpointWebExtension ...@@ -95,10 +96,10 @@ public class ReactiveHealthEndpointWebExtension
@Override @Override
protected Mono<? extends HealthComponent> aggregateContributions( protected Mono<? extends HealthComponent> aggregateContributions(
Map<String, Mono<? extends HealthComponent>> contributions, StatusAggregator statusAggregator, Map<String, Mono<? extends HealthComponent>> contributions, StatusAggregator statusAggregator,
boolean includeDetails) { boolean includeDetails, Set<String> groupNames) {
return Flux.fromIterable(contributions.entrySet()).flatMap(NamedHealthComponent::create) return Flux.fromIterable(contributions.entrySet()).flatMap(NamedHealthComponent::create)
.collectMap(NamedHealthComponent::getName, NamedHealthComponent::getHealth) .collectMap(NamedHealthComponent::getName, NamedHealthComponent::getHealth)
.map((components) -> this.getCompositeHealth(components, statusAggregator, includeDetails)); .map((components) -> this.getCompositeHealth(components, statusAggregator, includeDetails, groupNames));
} }
/** /**
......
/*
* 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;
}
}
/*
* 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 @@ ...@@ -16,6 +16,7 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
...@@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test; ...@@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
...@@ -44,7 +46,12 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T ...@@ -44,7 +46,12 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
final Health down = Health.down().build(); 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() { HealthEndpointSupportTests() {
this.registry = createRegistry(); this.registry = createRegistry();
...@@ -57,65 +64,76 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T ...@@ -57,65 +64,76 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test @Test
void createWhenRegistryIsNullThrowsException() { void createWhenRegistryIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> create(null, this.settings)) assertThatIllegalArgumentException().isThrownBy(() -> create(null, this.groups))
.withMessage("Registry must not be null"); .withMessage("Registry must not be null");
} }
@Test @Test
void createWhenSettingsIsNullThrowsException() { void createWhenGroupsIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> create(this.registry, null)) assertThatIllegalArgumentException().isThrownBy(() -> create(this.registry, null))
.withMessage("Settings must not be null"); .withMessage("Groups must not be null");
} }
@Test @Test
void getHealthWhenPathIsEmptyReturnsHealth() { void getHealthResultWhenPathIsEmptyUsesPrimaryGroup() {
this.registry.registerContributor("test", createContributor(this.up)); 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)).isNotSameAs(this.up);
assertThat(getHealth(result).getStatus()).isEqualTo(Status.UP); assertThat(getHealth(result).getStatus()).isEqualTo(Status.UP);
} }
@Test @Test
void getHealthWhenHasPathReturnsSubResult() { void getHealthResultWhenPathIsNotGroupReturnsResultFromPrimaryGroup() {
this.registry.registerContributor("test", createContributor(this.up)); 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); assertThat(getHealth(result)).isEqualTo(this.up);
} }
@Test @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)); 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"); assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot");
} }
@Test @Test
void getHealthWhenAlwaysIncludesDetailsIsFalseAndSettingsIsFalseIncludesNoDetails() { void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsFalseIncludesNoDetails() {
this.settings.setIncludeDetails(false); this.primaryGroup.setIncludeDetails(false);
this.registry.registerContributor("test", createContributor(this.up)); this.registry.registerContributor("test", createContributor(this.up));
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.settings); HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
T rootResult = endpoint.getHealth(SecurityContext.NONE, false); HealthResult<T> rootResult = endpoint.getHealth(SecurityContext.NONE, false);
T componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test"); HealthResult<T> componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test");
assertThat(((CompositeHealth) getHealth(rootResult)).getStatus()).isEqualTo(Status.UP); assertThat(((CompositeHealth) getHealth(rootResult)).getStatus()).isEqualTo(Status.UP);
assertThat(componentResult).isNull(); assertThat(componentResult).isNull();
} }
@Test @Test
void getHealthWhenAlwaysIncludesDetailsIsTrueIncludesDetails() { void getHealthResultWhenAlwaysIncludesDetailsIsTrueIncludesDetails() {
this.settings.setIncludeDetails(false); this.primaryGroup.setIncludeDetails(false);
this.registry.registerContributor("test", createContributor(this.up)); 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"); assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot");
} }
@Test @Test
void getHealthWhenCompositeReturnsAggregateResult() { void getHealthResultWhenCompositeReturnsAggregateResult() {
Map<String, C> contributors = new LinkedHashMap<>(); Map<String, C> contributors = new LinkedHashMap<>();
contributors.put("a", createContributor(this.up)); contributors.put("a", createContributor(this.up));
contributors.put("b", createContributor(this.down)); contributors.put("b", createContributor(this.down));
this.registry.registerContributor("test", createCompositeContributor(contributors)); 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 root = (CompositeHealth) getHealth(result);
CompositeHealth component = (CompositeHealth) root.getDetails().get("test"); CompositeHealth component = (CompositeHealth) root.getDetails().get("test");
assertThat(root.getStatus()).isEqualTo(Status.DOWN); assertThat(root.getStatus()).isEqualTo(Status.DOWN);
...@@ -124,12 +142,26 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T ...@@ -124,12 +142,26 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
} }
@Test @Test
void getHealthWhenPathDoesNotExistReturnsNull() { void getHealthResultWhenPathDoesNotExistReturnsNull() {
T result = create(this.registry, this.settings).getHealth(SecurityContext.NONE, false, "missing"); HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "missing");
assertThat(result).isNull(); 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(); protected abstract R createRegistry();
...@@ -137,6 +169,6 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T ...@@ -137,6 +169,6 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
protected abstract C createCompositeContributor(Map<String, C> contributors); 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; ...@@ -20,6 +20,8 @@ import java.util.Map;
import org.junit.jupiter.api.Test; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
...@@ -42,30 +44,30 @@ class HealthEndpointTests ...@@ -42,30 +44,30 @@ class HealthEndpointTests
} }
@Test @Test
void healthReturnsCompositeHealth() { void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up)); 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.getStatus()).isEqualTo(Status.UP);
assertThat(health).isInstanceOf(CompositeHealth.class); assertThat(health).isInstanceOf(SystemHealth.class);
} }
@Test @Test
void healthWhenPathDoesNotExistReturnsNull() { void healthWhenPathDoesNotExistReturnsNull() {
this.registry.registerContributor("test", createContributor(this.up)); 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(); assertThat(health).isNull();
} }
@Test @Test
void healthWhenPathExistsReturnsHealth() { void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up)); 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); assertThat(health).isEqualTo(this.up);
} }
@Override @Override
protected HealthEndpoint create(HealthContributorRegistry registry, HealthEndpointSettings settings) { protected HealthEndpoint create(HealthContributorRegistry registry, HealthEndpointGroups groups) {
return new HealthEndpoint(registry, settings); return new HealthEndpoint(registry, groups);
} }
@Override @Override
...@@ -84,8 +86,8 @@ class HealthEndpointTests ...@@ -84,8 +86,8 @@ class HealthEndpointTests
} }
@Override @Override
protected HealthComponent getHealth(HealthComponent result) { protected HealthComponent getHealth(HealthResult<HealthComponent> result) {
return result; return result.getHealth();
} }
} }
...@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; ...@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
...@@ -49,19 +50,18 @@ class HealthEndpointWebExtensionTests ...@@ -49,19 +50,18 @@ class HealthEndpointWebExtensionTests
@Test @Test
void healthReturnsSystemHealth() { void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up)); this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.settings) WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE);
.health(SecurityContext.NONE);
HealthComponent health = response.getBody(); HealthComponent health = response.getBody();
assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health).isInstanceOf(CompositeHealth.class); assertThat(health).isInstanceOf(SystemHealth.class);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@Test @Test
void healthWhenPathDoesNotExistReturnsHttp404() { void healthWhenPathDoesNotExistReturnsHttp404() {
this.registry.registerContributor("test", createContributor(this.up)); this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.settings) WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE,
.health(SecurityContext.NONE, "missing"); "missing");
assertThat(response.getBody()).isNull(); assertThat(response.getBody()).isNull();
assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getStatus()).isEqualTo(404);
} }
...@@ -69,15 +69,15 @@ class HealthEndpointWebExtensionTests ...@@ -69,15 +69,15 @@ class HealthEndpointWebExtensionTests
@Test @Test
void healthWhenPathExistsReturnsHealth() { void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up)); this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.settings) WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE,
.health(SecurityContext.NONE, "test"); "test");
assertThat(response.getBody()).isEqualTo(this.up); assertThat(response.getBody()).isEqualTo(this.up);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@Override @Override
protected HealthEndpointWebExtension create(HealthContributorRegistry registry, HealthEndpointSettings settings) { protected HealthEndpointWebExtension create(HealthContributorRegistry registry, HealthEndpointGroups groups) {
return new HealthEndpointWebExtension(registry, settings); return new HealthEndpointWebExtension(registry, groups);
} }
@Override @Override
...@@ -96,8 +96,8 @@ class HealthEndpointWebExtensionTests ...@@ -96,8 +96,8 @@ class HealthEndpointWebExtensionTests
} }
@Override @Override
protected HealthComponent getHealth(HealthComponent result) { protected HealthComponent getHealth(HealthResult<HealthComponent> result) {
return result; return result.getHealth();
} }
} }
...@@ -156,28 +156,30 @@ class HealthEndpointWebIntegrationTests { ...@@ -156,28 +156,30 @@ class HealthEndpointWebIntegrationTests {
@Bean @Bean
HealthEndpoint healthEndpoint(HealthContributorRegistry healthContributorRegistry, HealthEndpoint healthEndpoint(HealthContributorRegistry healthContributorRegistry,
HealthEndpointSettings healthEndpointSettings) { HealthEndpointGroups healthEndpointGroups) {
return new HealthEndpoint(healthContributorRegistry, healthEndpointSettings); return new HealthEndpoint(healthContributorRegistry, healthEndpointGroups);
} }
@Bean @Bean
@ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnWebApplication(type = Type.SERVLET)
HealthEndpointWebExtension healthWebEndpointExtension(HealthContributorRegistry healthContributorRegistry, HealthEndpointWebExtension healthWebEndpointExtension(HealthContributorRegistry healthContributorRegistry,
HealthEndpointSettings healthEndpointSettings) { HealthEndpointGroups healthEndpointGroups) {
return new HealthEndpointWebExtension(healthContributorRegistry, healthEndpointSettings); return new HealthEndpointWebExtension(healthContributorRegistry, healthEndpointGroups);
} }
@Bean @Bean
@ConditionalOnWebApplication(type = Type.REACTIVE) @ConditionalOnWebApplication(type = Type.REACTIVE)
ReactiveHealthEndpointWebExtension reactiveHealthWebEndpointExtension( ReactiveHealthEndpointWebExtension reactiveHealthWebEndpointExtension(
ReactiveHealthContributorRegistry reactiveHealthContributorRegistry, ReactiveHealthContributorRegistry reactiveHealthContributorRegistry,
HealthEndpointSettings healthEndpointSettings) { HealthEndpointGroups healthEndpointGroups) {
return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, healthEndpointSettings); return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, healthEndpointGroups);
} }
@Bean @Bean
HealthEndpointSettings healthEndpointSettings() { HealthEndpointGroups healthEndpointGroups() {
return new TestHealthEndpointSettings(); TestHealthEndpointGroup primary = new TestHealthEndpointGroup();
TestHealthEndpointGroup allTheAs = new TestHealthEndpointGroup((name) -> name.startsWith("a"));
return HealthEndpointGroups.of(primary, Collections.singletonMap("alltheas", allTheAs));
} }
@Bean @Bean
......
...@@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; ...@@ -23,6 +23,7 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
...@@ -50,18 +51,18 @@ class ReactiveHealthEndpointWebExtensionTests extends ...@@ -50,18 +51,18 @@ class ReactiveHealthEndpointWebExtensionTests extends
@Test @Test
void healthReturnsSystemHealth() { void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up)); 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(); .health(SecurityContext.NONE).block();
HealthComponent health = response.getBody(); HealthComponent health = response.getBody();
assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health).isInstanceOf(CompositeHealth.class); assertThat(health).isInstanceOf(SystemHealth.class);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@Test @Test
void healthWhenPathDoesNotExistReturnsHttp404() { void healthWhenPathDoesNotExistReturnsHttp404() {
this.registry.registerContributor("test", createContributor(this.up)); 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(); .health(SecurityContext.NONE, "missing").block();
assertThat(response.getBody()).isNull(); assertThat(response.getBody()).isNull();
assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getStatus()).isEqualTo(404);
...@@ -70,7 +71,7 @@ class ReactiveHealthEndpointWebExtensionTests extends ...@@ -70,7 +71,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
@Test @Test
void healthWhenPathExistsReturnsHealth() { void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up)); 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(); .health(SecurityContext.NONE, "test").block();
assertThat(response.getBody()).isEqualTo(this.up); assertThat(response.getBody()).isEqualTo(this.up);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
...@@ -78,8 +79,8 @@ class ReactiveHealthEndpointWebExtensionTests extends ...@@ -78,8 +79,8 @@ class ReactiveHealthEndpointWebExtensionTests extends
@Override @Override
protected ReactiveHealthEndpointWebExtension create(ReactiveHealthContributorRegistry registry, protected ReactiveHealthEndpointWebExtension create(ReactiveHealthContributorRegistry registry,
HealthEndpointSettings settings) { HealthEndpointGroups groups) {
return new ReactiveHealthEndpointWebExtension(registry, settings); return new ReactiveHealthEndpointWebExtension(registry, groups);
} }
@Override @Override
...@@ -99,8 +100,8 @@ class ReactiveHealthEndpointWebExtensionTests extends ...@@ -99,8 +100,8 @@ class ReactiveHealthEndpointWebExtensionTests extends
} }
@Override @Override
protected HealthComponent getHealth(Mono<? extends HealthComponent> result) { protected HealthComponent getHealth(HealthResult<Mono<? extends HealthComponent>> result) {
return result.block(); return result.getHealth().block();
} }
} }
/*
* 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 @@ ...@@ -16,21 +16,38 @@
package org.springframework.boot.actuate.health; package org.springframework.boot.actuate.health;
import java.util.function.Predicate;
import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.SecurityContext;
/** /**
* Test implementation of {@link HealthEndpointSettings}. * Test implementation of {@link HealthEndpointGroups}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
class TestHealthEndpointSettings implements HealthEndpointSettings { class TestHealthEndpointGroup implements HealthEndpointGroup {
private final StatusAggregator statusAggregator = new SimpleStatusAggregator(); private final StatusAggregator statusAggregator = new SimpleStatusAggregator();
private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(); private final HttpCodeStatusMapper httpCodeStatusMapper = new SimpleHttpCodeStatusMapper();
private final Predicate<String> memberPredicate;
private boolean includeDetails = true; 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 @Override
public boolean includeDetails(SecurityContext securityContext) { public boolean includeDetails(SecurityContext securityContext) {
return this.includeDetails; return this.includeDetails;
......
...@@ -951,6 +951,42 @@ TIP: If necessary, reactive indicators replace the regular ones. Also, any ...@@ -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]] [[production-ready-application-info]]
=== Application Information === Application Information
Application information exposes various information collected from all Application information exposes various information collected from all
......
...@@ -19,3 +19,8 @@ spring.jmx.enabled=true ...@@ -19,3 +19,8 @@ spring.jmx.enabled=true
spring.jackson.serialization.write_dates_as_timestamps=false spring.jackson.serialization.write_dates_as_timestamps=false
management.trace.http.include=request-headers,response-headers,principal,remote-address,session-id 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 { ...@@ -75,7 +75,7 @@ class ManagementPortAndPathSampleActuatorApplicationTests {
ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", getPassword()) ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", getPassword())
.getForEntity("http://localhost:" + this.managementPort + "/admin/health", String.class); .getForEntity("http://localhost:" + this.managementPort + "/admin/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\"}"); assertThat(entity.getBody()).isEqualTo("{\"status\":\"UP\",\"groups\":[\"live\",\"ready\"]}");
} }
@Test @Test
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment