Rework some aspects of the probes support

Closes gh-20962
pull/20966/head
Phillip Webb 5 years ago
commit 24ee9737c0

@ -0,0 +1,96 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.boot.actuate.autoconfigure.availability.AvailabilityProbesAutoConfiguration.ProbesCondition;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.availability.LivenessStateHealthIndicator;
import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link EnableAutoConfiguration Auto-configuration} for availability probes.
*
* @author Brian Clozel
* @author Phillip Webb
* @since 2.3.0
*/
@Configuration(proxyBeanMethods = false)
@Conditional(ProbesCondition.class)
@AutoConfigureAfter(ApplicationAvailabilityAutoConfiguration.class)
public class AvailabilityProbesAutoConfiguration {
@Bean
@ConditionalOnEnabledHealthIndicator("livenessState")
@ConditionalOnMissingBean
public LivenessStateHealthIndicator livenessStateHealthIndicator(ApplicationAvailability applicationAvailability) {
return new LivenessStateHealthIndicator(applicationAvailability);
}
@Bean
@ConditionalOnEnabledHealthIndicator("readinessState")
@ConditionalOnMissingBean
public ReadinessStateHealthIndicator readinessStateHealthIndicator(
ApplicationAvailability applicationAvailability) {
return new ReadinessStateHealthIndicator(applicationAvailability);
}
@Bean
public AvailabilityProbesHealthEndpointGroupsPostProcessor availabilityProbesHealthEndpointGroupsPostProcessor() {
return new AvailabilityProbesHealthEndpointGroupsPostProcessor();
}
/**
* {@link SpringBootCondition} to enable or disable probes.
*/
static class ProbesCondition extends SpringBootCondition {
private static final String ENABLED_PROPERTY = "management.health.probes.enabled";
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
ConditionMessage.Builder message = ConditionMessage.forCondition("Health availability");
String enabled = environment.getProperty(ENABLED_PROPERTY);
if (enabled != null) {
boolean match = !"false".equalsIgnoreCase(enabled);
return new ConditionOutcome(match,
message.because("'" + ENABLED_PROPERTY + "' set to '" + enabled + "'"));
}
if (CloudPlatform.getActive(environment) == CloudPlatform.KUBERNETES) {
return ConditionOutcome.match(message.because("running on Kubernetes"));
}
return ConditionOutcome.noMatch(message.because("not running on a supported cloud platform"));
}
}
}

@ -0,0 +1,67 @@
/*
* Copyright 2012-2020 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.availability;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
/**
* {@link HealthEndpointGroup} used to support availability probes.
*
* @author Phillip Webb
* @author Brian Clozel
*/
class AvailabilityProbesHealthEndpointGroup implements HealthEndpointGroup {
private final Set<String> members;
AvailabilityProbesHealthEndpointGroup(String... members) {
this.members = new HashSet<>(Arrays.asList(members));
}
@Override
public boolean isMember(String name) {
return this.members.contains(name);
}
@Override
public boolean showComponents(SecurityContext securityContext) {
return false;
}
@Override
public boolean showDetails(SecurityContext securityContext) {
return false;
}
@Override
public StatusAggregator getStatusAggregator() {
return StatusAggregator.DEFAULT;
}
@Override
public HttpCodeStatusMapper getHttpCodeStatusMapper() {
return HttpCodeStatusMapper.DEFAULT;
}
}

@ -0,0 +1,80 @@
/*
* Copyright 2012-2020 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.availability;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.util.Assert;
/**
* {@link HealthEndpointGroups} decorator to support availability probes.
*
* @author Phillip Webb
* @author Brian Clozel
*/
class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups {
private static final Map<String, AvailabilityProbesHealthEndpointGroup> GROUPS;
static {
Map<String, AvailabilityProbesHealthEndpointGroup> groups = new LinkedHashMap<>();
groups.put("liveness", new AvailabilityProbesHealthEndpointGroup("livenessState"));
groups.put("readiness", new AvailabilityProbesHealthEndpointGroup("readinessState"));
GROUPS = Collections.unmodifiableMap(groups);
}
private final HealthEndpointGroups groups;
private final Set<String> names;
AvailabilityProbesHealthEndpointGroups(HealthEndpointGroups groups) {
Assert.notNull(groups, "Groups must not be null");
this.groups = groups;
Set<String> names = new LinkedHashSet<>(groups.getNames());
names.addAll(GROUPS.keySet());
this.names = Collections.unmodifiableSet(names);
}
@Override
public HealthEndpointGroup getPrimary() {
return this.groups.getPrimary();
}
@Override
public Set<String> getNames() {
return this.names;
}
@Override
public HealthEndpointGroup get(String name) {
HealthEndpointGroup group = this.groups.get(name);
if (group == null) {
group = GROUPS.get(name);
}
return group;
}
static boolean containsAllProbeGroups(HealthEndpointGroups groups) {
return groups.getNames().containsAll(GROUPS.keySet());
}
}

@ -0,0 +1,41 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* {@link HealthEndpointGroupsPostProcessor} to add
* {@link AvailabilityProbesHealthEndpointGroups}.
*
* @author Phillip Webb
*/
@Order(Ordered.LOWEST_PRECEDENCE)
class AvailabilityProbesHealthEndpointGroupsPostProcessor implements HealthEndpointGroupsPostProcessor {
@Override
public HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups) {
if (AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(groups)) {
return groups;
}
return new AvailabilityProbesHealthEndpointGroups(groups);
}
}

@ -15,6 +15,7 @@
*/
/**
* Auto-configuration for actuator kubernetes concerns.
* Auto-configuration that extends health endpoints so that they can be used as
* availability probes.
*/
package org.springframework.boot.actuate.autoconfigure.kubernetes;
package org.springframework.boot.actuate.autoconfigure.availability;

@ -20,6 +20,7 @@ import java.security.Principal;
import java.util.Collection;
import java.util.function.Predicate;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
@ -30,12 +31,12 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/**
* Auto-configured {@link HealthEndpointGroup}.
* Auto-configured {@link HealthEndpointGroup} backed by {@link HealthProperties}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class DefaultHealthEndpointGroup implements HealthEndpointGroup {
class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
private final Predicate<String> members;
@ -50,7 +51,7 @@ class DefaultHealthEndpointGroup implements HealthEndpointGroup {
private final Collection<String> roles;
/**
* Create a new {@link DefaultHealthEndpointGroup} instance.
* Create a new {@link AutoConfiguredHealthEndpointGroup} instance.
* @param members a predicate used to test for group membership
* @param statusAggregator the status aggregator to use
* @param httpCodeStatusMapper the HTTP code status mapper to use
@ -58,7 +59,7 @@ class DefaultHealthEndpointGroup implements HealthEndpointGroup {
* @param showDetails the show details setting
* @param roles the roles to match
*/
DefaultHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
AutoConfiguredHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails,
Collection<String> roles) {
this.members = members;

@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
@ -32,12 +31,10 @@ 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.Show;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroup.Show;
import org.springframework.boot.actuate.health.HealthEndpointGroupConfigurer;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper;
import org.springframework.boot.actuate.health.SimpleStatusAggregator;
@ -48,113 +45,71 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* Auto-configured {@link HealthEndpointGroupsRegistry}.
* Auto-configured {@link HealthEndpointGroups}.
*
* @author Phillip Webb
* @author Brian Clozel
*/
class AutoConfiguredHealthEndpointGroupsRegistry implements HealthEndpointGroupsRegistry {
class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
private static Predicate<String> ALL = (name) -> true;
private final StatusAggregator defaultStatusAggregator;
private final HttpCodeStatusMapper defaultHttpCodeStatusMapper;
private final Show defaultShowComponents;
private final Show defaultShowDetails;
private final Set<String> defaultRoles;
private HealthEndpointGroup primaryGroup;
private final HealthEndpointGroup primaryGroup;
private final Map<String, HealthEndpointGroup> groups;
/**
* Create a new {@link AutoConfiguredHealthEndpointGroupsRegistry} instance.
* Create a new {@link AutoConfiguredHealthEndpointGroups} instance.
* @param applicationContext the application context used to check for override beans
* @param properties the health endpoint properties
*/
AutoConfiguredHealthEndpointGroupsRegistry(ApplicationContext applicationContext,
HealthEndpointProperties properties) {
AutoConfiguredHealthEndpointGroups(ApplicationContext applicationContext, HealthEndpointProperties properties) {
ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext)
? ((ConfigurableApplicationContext) applicationContext).getBeanFactory() : applicationContext;
this.defaultShowComponents = convertVisibility(properties.getShowComponents());
this.defaultShowDetails = convertVisibility(properties.getShowDetails());
this.defaultRoles = properties.getRoles();
Show showComponents = properties.getShowComponents();
Show showDetails = properties.getShowDetails();
Set<String> roles = properties.getRoles();
StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class);
if (statusAggregator == null) {
statusAggregator = new SimpleStatusAggregator(properties.getStatus().getOrder());
}
this.defaultStatusAggregator = statusAggregator;
HttpCodeStatusMapper httpCodeStatusMapper = getNonQualifiedBean(beanFactory, HttpCodeStatusMapper.class);
if (httpCodeStatusMapper == null) {
httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping());
}
this.defaultHttpCodeStatusMapper = httpCodeStatusMapper;
this.primaryGroup = new DefaultHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper,
this.defaultShowComponents, this.defaultShowDetails, this.defaultRoles);
this.groups = createGroups(properties.getGroup(), beanFactory);
}
@Override
public HealthEndpointGroupsRegistry add(String groupName, Consumer<HealthEndpointGroupConfigurer> consumer) {
DefaultHealthEndpointGroupConfigurer groupConfigurer = new DefaultHealthEndpointGroupConfigurer(
this.defaultStatusAggregator, this.defaultHttpCodeStatusMapper, this.defaultShowComponents,
this.defaultShowDetails, this.defaultRoles);
consumer.accept(groupConfigurer);
this.groups.put(groupName, groupConfigurer.toHealthEndpointGroup());
return this;
}
@Override
public HealthEndpointGroupsRegistry remove(String groupName) {
this.groups.remove(groupName);
return this;
this.primaryGroup = new AutoConfiguredHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper,
showComponents, showDetails, roles);
this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, httpCodeStatusMapper,
showComponents, showDetails, roles);
}
@Override
public HealthEndpointGroups toGroups() {
return HealthEndpointGroups.of(this.primaryGroup, Collections.unmodifiableMap(this.groups));
}
private Map<String, HealthEndpointGroup> createGroups(Map<String, Group> groupProperties, BeanFactory beanFactory) {
private Map<String, HealthEndpointGroup> createGroups(Map<String, Group> groupProperties, BeanFactory beanFactory,
StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper,
Show defaultShowComponents, Show defaultShowDetails, Set<String> defaultRoles) {
Map<String, HealthEndpointGroup> groups = new LinkedHashMap<>();
groupProperties
.forEach((groupName, group) -> groups.put(groupName, createGroup(groupName, group, beanFactory)));
return groups;
}
private HealthEndpointGroup createGroup(String groupName, Group groupProperties, BeanFactory beanFactory) {
Status status = groupProperties.getStatus();
Show showComponents = (groupProperties.getShowComponents() != null)
? convertVisibility(groupProperties.getShowComponents()) : this.defaultShowComponents;
Show showDetails = (groupProperties.getShowDetails() != null)
? convertVisibility(groupProperties.getShowDetails()) : this.defaultShowDetails;
Set<String> roles = !CollectionUtils.isEmpty(groupProperties.getRoles()) ? groupProperties.getRoles()
: this.defaultRoles;
groupProperties.forEach((groupName, group) -> {
Status status = group.getStatus();
Show showComponents = (group.getShowComponents() != null) ? group.getShowComponents()
: defaultShowComponents;
Show showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails;
Set<String> roles = !CollectionUtils.isEmpty(group.getRoles()) ? group.getRoles() : defaultRoles;
StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, groupName, () -> {
if (!CollectionUtils.isEmpty(status.getOrder())) {
return new SimpleStatusAggregator(status.getOrder());
}
return this.defaultStatusAggregator;
return defaultStatusAggregator;
});
HttpCodeStatusMapper httpCodeStatusMapper = getQualifiedBean(beanFactory, HttpCodeStatusMapper.class, groupName,
() -> {
HttpCodeStatusMapper httpCodeStatusMapper = getQualifiedBean(beanFactory, HttpCodeStatusMapper.class,
groupName, () -> {
if (!CollectionUtils.isEmpty(status.getHttpMapping())) {
return new SimpleHttpCodeStatusMapper(status.getHttpMapping());
}
return this.defaultHttpCodeStatusMapper;
return defaultHttpCodeStatusMapper;
});
Predicate<String> members = new IncludeExcludeGroupMemberPredicate(groupProperties.getInclude(),
groupProperties.getExclude());
return new DefaultHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper, showComponents,
showDetails, roles);
Predicate<String> members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude());
groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper,
showComponents, showDetails, roles));
});
return Collections.unmodifiableMap(groups);
}
private <T> T getNonQualifiedBean(ListableBeanFactory beanFactory, Class<T> type) {
@ -185,21 +140,6 @@ class AutoConfiguredHealthEndpointGroupsRegistry implements HealthEndpointGroups
}
}
private Show convertVisibility(HealthProperties.Show show) {
if (show == null) {
return null;
}
switch (show) {
case ALWAYS:
return Show.ALWAYS;
case NEVER:
return Show.NEVER;
case WHEN_AUTHORIZED:
return Show.WHEN_AUTHORIZED;
}
throw new IllegalStateException("Unsupported 'show' value " + show);
}
@Override
public HealthEndpointGroup getPrimary() {
return this.primaryGroup;

@ -1,110 +0,0 @@
/*
* Copyright 2012-2020 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.HashSet;
import java.util.Set;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroup.Show;
import org.springframework.boot.actuate.health.HealthEndpointGroupConfigurer;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
/**
* Mutable {@link HealthEndpointGroupConfigurer configurer} for
* {@link HealthEndpointGroup}.
*
* @author Brian Clozel
*/
class DefaultHealthEndpointGroupConfigurer implements HealthEndpointGroupConfigurer {
Set<String> includedIndicators;
Set<String> excludedIndicators;
private StatusAggregator statusAggregator;
private HttpCodeStatusMapper httpCodeStatusMapper;
private Show showComponents;
private Show showDetails;
private Set<String> roles;
DefaultHealthEndpointGroupConfigurer(StatusAggregator defaultStatusAggregator,
HttpCodeStatusMapper defaultHttpCodeStatusMapper, Show defaultShowComponents, Show defaultShowDetails,
Set<String> defaultRoles) {
this.statusAggregator = defaultStatusAggregator;
this.httpCodeStatusMapper = defaultHttpCodeStatusMapper;
this.showComponents = defaultShowComponents;
this.showDetails = defaultShowDetails;
this.roles = new HashSet<>(defaultRoles);
}
@Override
public HealthEndpointGroupConfigurer include(String... indicators) {
this.includedIndicators = new HashSet<>(Arrays.asList(indicators));
return this;
}
@Override
public HealthEndpointGroupConfigurer exclude(String... exclude) {
this.excludedIndicators = new HashSet<>(Arrays.asList(exclude));
return this;
}
@Override
public HealthEndpointGroupConfigurer statusAggregator(StatusAggregator statusAggregator) {
this.statusAggregator = statusAggregator;
return this;
}
@Override
public HealthEndpointGroupConfigurer httpCodeStatusMapper(HttpCodeStatusMapper httpCodeStatusMapper) {
this.httpCodeStatusMapper = httpCodeStatusMapper;
return this;
}
@Override
public HealthEndpointGroupConfigurer showComponents(Show showComponents) {
this.showComponents = showComponents;
return this;
}
@Override
public HealthEndpointGroupConfigurer showDetails(Show showDetails) {
this.showDetails = showDetails;
return this;
}
@Override
public HealthEndpointGroupConfigurer roles(String... roles) {
this.roles = new HashSet<>(Arrays.asList(roles));
return this;
}
HealthEndpointGroup toHealthEndpointGroup() {
IncludeExcludeGroupMemberPredicate predicate = new IncludeExcludeGroupMemberPredicate(this.includedIndicators,
this.excludedIndicators);
return new DefaultHealthEndpointGroup(predicate, this.statusAggregator, this.httpCodeStatusMapper,
this.showComponents, this.showDetails, this.roles);
}
}

@ -21,7 +21,9 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor;
import org.springframework.boot.actuate.health.Health;
@ -29,7 +31,7 @@ import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.NamedContributor;
@ -68,11 +70,8 @@ class HealthEndpointConfiguration {
@Bean
@ConditionalOnMissingBean
HealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext,
HealthEndpointProperties properties, ObjectProvider<HealthEndpointGroupsRegistryCustomizer> customizers) {
AutoConfiguredHealthEndpointGroupsRegistry registry = new AutoConfiguredHealthEndpointGroupsRegistry(
applicationContext, properties);
customizers.orderedStream().forEach((customizer) -> customizer.customize(registry));
return registry.toGroups();
HealthEndpointProperties properties) {
return new AutoConfiguredHealthEndpointGroups(applicationContext, properties);
}
@Bean
@ -93,6 +92,42 @@ class HealthEndpointConfiguration {
return new HealthEndpoint(registry, groups);
}
@Bean
static HealthEndpointGroupsBeanPostProcessor healthEndpointGroupsBeanPostProcessor(
ObjectProvider<HealthEndpointGroupsPostProcessor> healthEndpointGroupsPostProcessors) {
return new HealthEndpointGroupsBeanPostProcessor(healthEndpointGroupsPostProcessors);
}
/**
* {@link BeanPostProcessor} to invoke {@link HealthEndpointGroupsPostProcessor}
* beans.
*/
private static class HealthEndpointGroupsBeanPostProcessor implements BeanPostProcessor {
private final ObjectProvider<HealthEndpointGroupsPostProcessor> postProcessors;
HealthEndpointGroupsBeanPostProcessor(ObjectProvider<HealthEndpointGroupsPostProcessor> postProcessors) {
this.postProcessors = postProcessors;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HealthEndpointGroups) {
return applyPostProcessors((HealthEndpointGroups) bean);
}
return bean;
}
private Object applyPostProcessors(HealthEndpointGroups bean) {
for (HealthEndpointGroupsPostProcessor postProcessor : this.postProcessors.orderedStream()
.toArray(HealthEndpointGroupsPostProcessor[]::new)) {
bean = postProcessor.postProcessHealthEndpointGroups(bean);
}
return bean;
}
}
/**
* Adapter to expose {@link ReactiveHealthContributor} beans as
* {@link HealthContributor} instances.

@ -1,89 +0,0 @@
/*
* Copyright 2012-2020 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.kubernetes;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.autoconfigure.kubernetes.ProbesHealthContributorAutoConfiguration.KubernetesOrPropertyCondition;
import org.springframework.boot.actuate.availability.LivenessProbeHealthIndicator;
import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicator;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
import org.springframework.boot.actuate.kubernetes.ProbesHealthEndpointGroupsRegistrar;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for
* {@link LivenessProbeHealthIndicator} and {@link ReadinessProbeHealthIndicator}.
*
* @author Brian Clozel
* @since 2.3.0
*/
@Configuration(proxyBeanMethods = false)
@Conditional(KubernetesOrPropertyCondition.class)
@AutoConfigureAfter(ApplicationAvailabilityAutoConfiguration.class)
public class ProbesHealthContributorAutoConfiguration {
@Bean
@ConditionalOnEnabledHealthIndicator("livenessProbe")
@ConditionalOnMissingBean
public LivenessProbeHealthIndicator livenessProbeHealthIndicator(
ApplicationAvailabilityProvider applicationAvailabilityProvider) {
return new LivenessProbeHealthIndicator(applicationAvailabilityProvider);
}
@Bean
@ConditionalOnEnabledHealthIndicator("readinessProbe")
@ConditionalOnMissingBean
public ReadinessProbeHealthIndicator readinessProbeHealthIndicator(
ApplicationAvailabilityProvider applicationAvailabilityProvider) {
return new ReadinessProbeHealthIndicator(applicationAvailabilityProvider);
}
@Bean
public HealthEndpointGroupsRegistryCustomizer probesRegistryCustomizer() {
return new ProbesHealthEndpointGroupsRegistrar();
}
static class KubernetesOrPropertyCondition extends AnyNestedCondition {
KubernetesOrPropertyCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
static class Kubernetes {
}
@ConditionalOnProperty(prefix = "management.health.probes", name = "enabled")
static class ProbesIndicatorsEnabled {
}
}
}

@ -2,6 +2,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.availability.AvailabilityProbesAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthContributorAutoConfiguration,\
@ -29,7 +30,6 @@ org.springframework.boot.actuate.autoconfigure.integration.IntegrationGraphEndpo
org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.jms.JmsHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.jolokia.JolokiaEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.kubernetes.ProbesHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.ldap.LdapHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration,\

@ -0,0 +1,76 @@
/*
* Copyright 2012-2020 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.availability;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.availability.LivenessStateHealthIndicator;
import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AvailabilityProbesAutoConfiguration}.
*
* @author Brian Clozel
*/
class AvailabilityProbesAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
.of(ApplicationAvailabilityAutoConfiguration.class, AvailabilityProbesAutoConfiguration.class));
@Test
void probesWhenNotKubernetesAddsNoBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class)
.doesNotHaveBean(LivenessStateHealthIndicator.class)
.doesNotHaveBean(ReadinessStateHealthIndicator.class)
.doesNotHaveBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class));
}
@Test
void probesWhenKubernetesAddsBeans() {
this.contextRunner.withPropertyValues("spring.main.cloud-platform=kubernetes")
.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class)
.hasSingleBean(LivenessStateHealthIndicator.class)
.hasSingleBean(ReadinessStateHealthIndicator.class)
.hasSingleBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class));
}
@Test
void probesWhenPropertyEnabledAddsBeans() {
this.contextRunner.withPropertyValues("management.health.probes.enabled=true")
.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class)
.hasSingleBean(LivenessStateHealthIndicator.class)
.hasSingleBean(ReadinessStateHealthIndicator.class)
.hasSingleBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class));
}
@Test
void probesWhenKuberntesAndPropertyDisabledAddsNotBeans() {
this.contextRunner
.withPropertyValues("spring.main.cloud-platform=kubernetes", "management.health.probes.enabled=false")
.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class)
.doesNotHaveBean(LivenessStateHealthIndicator.class)
.doesNotHaveBean(ReadinessStateHealthIndicator.class)
.doesNotHaveBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class));
}
}

@ -0,0 +1,68 @@
/*
* Copyright 2012-2020 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.availability;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AvailabilityProbesHealthEndpointGroup}.
*
* @author Phillip Webb
*/
class AvailabilityProbesHealthEndpointGroupTests {
private AvailabilityProbesHealthEndpointGroup group = new AvailabilityProbesHealthEndpointGroup("a", "b");
@Test
void isMemberWhenMemberReturnsTrue() {
assertThat(this.group.isMember("a")).isTrue();
assertThat(this.group.isMember("b")).isTrue();
}
@Test
void isMemberWhenNotMemberReturnsFalse() {
assertThat(this.group.isMember("c")).isFalse();
}
@Test
void showComponentsReturnsFalse() {
assertThat(this.group.showComponents(mock(SecurityContext.class))).isFalse();
}
@Test
void showDetailsReturnsFalse() {
assertThat(this.group.showDetails(mock(SecurityContext.class))).isFalse();
}
@Test
void getStatusAggregattorReturnsDefaultStatusAggregator() {
assertThat(this.group.getStatusAggregator()).isEqualTo(StatusAggregator.DEFAULT);
}
@Test
void getHttpCodeStatusMapperReturnsDefaultHttpCodeStatusMapper() {
assertThat(this.group.getHttpCodeStatusMapper()).isEqualTo(HttpCodeStatusMapper.DEFAULT);
}
}

@ -0,0 +1,74 @@
/*
* Copyright 2012-2020 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.availability;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AvailabilityProbesHealthEndpointGroupsPostProcessor}.
*
* @author Phillip Webb
*/
class AvailabilityProbesHealthEndpointGroupsPostProcessorTests {
private AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor();
@Test
void postProcessHealthEndpointGroupsWhenGroupsAlreadyContainedReturnsOriginal() {
HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
Set<String> names = new LinkedHashSet<>();
names.add("test");
names.add("readiness");
names.add("liveness");
given(groups.getNames()).willReturn(names);
assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups)).isSameAs(groups);
}
@Test
void postProcessHealthEndpointGroupsWhenGroupContainsOneReturnsPostProcessed() {
HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
Set<String> names = new LinkedHashSet<>();
names.add("test");
names.add("readiness");
given(groups.getNames()).willReturn(names);
assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups))
.isInstanceOf(AvailabilityProbesHealthEndpointGroups.class);
}
@Test
void postProcessHealthEndpointGroupsWhenGroupsContainsNoneReturnsProcessed() {
HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
Set<String> names = new LinkedHashSet<>();
names.add("test");
names.add("spring");
names.add("boot");
given(groups.getNames()).willReturn(names);
assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups))
.isInstanceOf(AvailabilityProbesHealthEndpointGroups.class);
}
}

@ -0,0 +1,124 @@
/*
* Copyright 2012-2020 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.availability;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AvailabilityProbesHealthEndpointGroups}.
*
* @author Phillip Webb
*/
class AvailabilityProbesHealthEndpointGroupsTests {
private HealthEndpointGroups delegate;
private HealthEndpointGroup group;
@BeforeEach
void setup() {
this.delegate = mock(HealthEndpointGroups.class);
this.group = mock(HealthEndpointGroup.class);
}
@Test
void createWhenGroupsIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new AvailabilityProbesHealthEndpointGroups(null))
.withMessage("Groups must not be null");
}
@Test
void getPrimaryDelegatesToGroups() {
given(this.delegate.getPrimary()).willReturn(this.group);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
assertThat(availabilityProbes.getPrimary()).isEqualTo(this.group);
}
@Test
void getNamesIncludesAvailabilityProbeGroups() {
given(this.delegate.getNames()).willReturn(Collections.singleton("test"));
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
assertThat(availabilityProbes.getNames()).containsExactly("test", "liveness", "readiness");
}
@Test
void getWhenProbeInDelegateReturnsGroupFromDelegate() {
given(this.delegate.get("liveness")).willReturn(this.group);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
assertThat(availabilityProbes.get("liveness")).isEqualTo(this.group);
}
@Test
void getWhenProbeNotInDelegateReturnsProbeGroup() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
assertThat(availabilityProbes.get("liveness")).isInstanceOf(AvailabilityProbesHealthEndpointGroup.class);
}
@Test
void getWhenNotProbeAndNotInDelegateReturnsNull() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
assertThat(availabilityProbes.get("mygroup")).isNull();
}
@Test
void getLivenessProbeHasOnlyLivenessStateAsMember() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroup probeGroup = availabilityProbes.get("liveness");
assertThat(probeGroup.isMember("livenessState")).isTrue();
assertThat(probeGroup.isMember("readinessState")).isFalse();
}
@Test
void getRedinessProbeHasOnlyReadinessStateAsMember() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroup probeGroup = availabilityProbes.get("readiness");
assertThat(probeGroup.isMember("livenessState")).isFalse();
assertThat(probeGroup.isMember("readinessState")).isTrue();
}
@Test
void containsAllWhenContainsAllReturnTrue() {
given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "liveness", "readiness")));
assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isTrue();
}
@Test
void containsAllWhenContainsOneReturnFalse() {
given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "liveness")));
assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isFalse();
}
@Test
void containsAllWhenContainsNoneReturnFalse() {
given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "spring")));
assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isFalse();
}
}

@ -25,8 +25,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.HealthEndpointGroup.Show;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.security.core.Authentication;
@ -37,11 +37,11 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultHealthEndpointGroup}.
* Tests for {@link AutoConfiguredHealthEndpointGroup}.
*
* @author Phillip Webb
*/
class DefaultHealthEndpointGroupTests {
class AutoConfiguredHealthEndpointGroupTests {
@Mock
private StatusAggregator statusAggregator;
@ -62,7 +62,7 @@ class DefaultHealthEndpointGroupTests {
@Test
void isMemberWhenMemberPredicateMatchesAcceptsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> name.startsWith("a"),
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.isMember("albert")).isTrue();
assertThat(group.isMember("arnold")).isTrue();
@ -70,7 +70,7 @@ class DefaultHealthEndpointGroupTests {
@Test
void isMemberWhenMemberPredicateRejectsReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> name.startsWith("a"),
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.isMember("bert")).isFalse();
assertThat(group.isMember("ernie")).isFalse();
@ -78,38 +78,39 @@ class DefaultHealthEndpointGroupTests {
@Test
void showDetailsWhenShowDetailsIsNeverReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
assertThat(group.showDetails(SecurityContext.NONE)).isFalse();
}
@Test
void showDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.showDetails(SecurityContext.NONE)).isTrue();
}
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(group.showDetails(this.securityContext)).isFalse();
}
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showDetails(this.securityContext)).isTrue();
}
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Arrays.asList("admin", "root", "bossmode"));
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.showDetails(this.securityContext)).isTrue();
@ -117,8 +118,9 @@ class DefaultHealthEndpointGroupTests {
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Arrays.asList("admin", "rot", "bossmode"));
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "rot", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.showDetails(this.securityContext)).isFalse();
@ -126,8 +128,9 @@ class DefaultHealthEndpointGroupTests {
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserHasRightAuthorityReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Arrays.asList("admin", "root", "bossmode"));
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin")));
@ -137,8 +140,9 @@ class DefaultHealthEndpointGroupTests {
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Arrays.asList("admin", "rot", "bossmode"));
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "rot", "bossmode"));
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other")));
@ -148,48 +152,50 @@ class DefaultHealthEndpointGroupTests {
@Test
void showComponentsWhenShowComponentsIsNullDelegatesToShowDetails() {
DefaultHealthEndpointGroup alwaysGroup = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
AutoConfiguredHealthEndpointGroup alwaysGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(alwaysGroup.showComponents(SecurityContext.NONE)).isTrue();
DefaultHealthEndpointGroup neverGroup = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
AutoConfiguredHealthEndpointGroup neverGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
assertThat(neverGroup.showComponents(SecurityContext.NONE)).isFalse();
}
@Test
void showComponentsWhenShowComponentsIsNeverReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet());
assertThat(group.showComponents(SecurityContext.NONE)).isFalse();
}
@Test
void showComponentsWhenShowComponentsIsAlwaysReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet());
assertThat(group.showComponents(SecurityContext.NONE)).isTrue();
}
@Test
void showComponentsWhenShowComponentsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(group.showComponents(this.securityContext)).isFalse();
}
@Test
void showComponentsWhenShowComponentsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Collections.emptySet());
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showComponents(this.securityContext)).isTrue();
}
@Test
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "root", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
@ -198,8 +204,9 @@ class DefaultHealthEndpointGroupTests {
@Test
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Arrays.asList("admin", "rot", "bossmode"));
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "rot", "bossmode"));
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("root")).willReturn(true);
assertThat(group.showComponents(this.securityContext)).isFalse();
@ -207,8 +214,8 @@ class DefaultHealthEndpointGroupTests {
@Test
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserHasRightAuthoritiesReturnsTrue() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "root", "bossmode"));
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
@ -219,8 +226,9 @@ class DefaultHealthEndpointGroupTests {
@Test
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Arrays.asList("admin", "rot", "bossmode"));
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "rot", "bossmode"));
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other")));
@ -230,15 +238,15 @@ class DefaultHealthEndpointGroupTests {
@Test
void getStatusAggregatorReturnsStatusAggregator() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.getStatusAggregator()).isSameAs(this.statusAggregator);
}
@Test
void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() {
DefaultHealthEndpointGroup group = new DefaultHealthEndpointGroup((name) -> true, this.statusAggregator,
this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
assertThat(group.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper);
}

@ -40,11 +40,11 @@ import org.springframework.context.annotation.Primary;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AutoConfiguredHealthEndpointGroupsRegistry}.
* Tests for {@link AutoConfiguredHealthEndpointGroups}.
*
* @author Phillip Webb
*/
class AutoConfiguredHealthEndpointGroupsBuilderTests {
class AutoConfiguredHealthEndpointGroupsTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AutoConfiguredHealthEndpointGroupsTestConfiguration.class));
@ -313,9 +313,9 @@ class AutoConfiguredHealthEndpointGroupsBuilderTests {
static class AutoConfiguredHealthEndpointGroupsTestConfiguration {
@Bean
AutoConfiguredHealthEndpointGroupsRegistry healthEndpointGroups(
ConfigurableApplicationContext applicationContext, HealthEndpointProperties properties) {
return new AutoConfiguredHealthEndpointGroupsRegistry(applicationContext, properties);
AutoConfiguredHealthEndpointGroups healthEndpointGroups(ConfigurableApplicationContext applicationContext,
HealthEndpointProperties properties) {
return new AutoConfiguredHealthEndpointGroups(applicationContext, properties);
}
}

@ -37,7 +37,7 @@ import org.springframework.boot.actuate.health.HealthComponent;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
@ -56,6 +56,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@ -155,6 +156,7 @@ class HealthEndpointAutoConfigurationTests {
void runCreatesHealthEndpointGroups() {
this.contextRunner.withPropertyValues("management.endpoint.health.group.ready.include=*").run((context) -> {
HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class);
assertThat(groups).isInstanceOf(AutoConfiguredHealthEndpointGroups.class);
assertThat(groups.getNames()).containsOnly("ready");
});
}
@ -302,11 +304,12 @@ class HealthEndpointAutoConfigurationTests {
}
@Test
void runWhenHealthEndpointGroupsRegistryCustomizerAddsHealthEndpointGroup() {
this.contextRunner.withUserConfiguration(HealthEndpointGroupsRegistryCustomizerConfig.class).run((context) -> {
assertThat(context).hasSingleBean(HealthEndpointGroupsRegistryCustomizer.class);
void runWhenHasHealthEndpointGroupsPostProcessorPerformsProcessing() {
this.contextRunner.withPropertyValues("management.endpoint.health.group.ready.include=*").withUserConfiguration(
HealthEndpointGroupsConfiguration.class, TestHealthEndpointGroupsPostProcessor.class).run((context) -> {
HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class);
assertThat(groups.getNames()).contains("test");
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> groups.get("test"))
.withMessage("postprocessed");
});
}
@ -429,12 +432,12 @@ class HealthEndpointAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class HealthEndpointGroupsRegistryCustomizerConfig {
static class TestHealthEndpointGroupsPostProcessor implements HealthEndpointGroupsPostProcessor {
@Bean
HealthEndpointGroupsRegistryCustomizer customHealthEndpointGroup() {
return (registry) -> registry.add("test", (configurer) -> configurer.include("ping"));
@Override
public HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups) {
given(groups.get("test")).willThrow(new RuntimeException("postprocessed"));
return groups;
}
}

@ -1,58 +0,0 @@
/*
* Copyright 2012-2020 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.kubernetes;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.availability.LivenessProbeHealthIndicator;
import org.springframework.boot.actuate.availability.ReadinessProbeHealthIndicator;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests fos {@link ProbesHealthContributorAutoConfiguration}.
*
* @author Brian Clozel
*/
class ProbesHealthContributorAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
.of(ApplicationAvailabilityAutoConfiguration.class, ProbesHealthContributorAutoConfiguration.class));
@Test
void probesNotConfiguredIfNotKubernetes() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class)
.doesNotHaveBean(LivenessProbeHealthIndicator.class)
.doesNotHaveBean(ReadinessProbeHealthIndicator.class)
.doesNotHaveBean(HealthEndpointGroupsRegistryCustomizer.class));
}
@Test
void probesConfiguredIfProperty() {
this.contextRunner.withPropertyValues("management.health.probes.enabled=true")
.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class)
.hasSingleBean(LivenessProbeHealthIndicator.class)
.hasSingleBean(ReadinessProbeHealthIndicator.class)
.hasSingleBean(HealthEndpointGroupsRegistryCustomizer.class));
}
}

@ -0,0 +1,122 @@
/*
* Copyright 2012-2020 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.availability;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.util.Assert;
/**
* A {@link HealthIndicator} that checks a specific {@link AvailabilityState} of the
* application.
*
* @author Phillip Webb
* @author Brian Clozel
* @since 2.3.0
*/
public class AvailabilityStateHealthIndicator extends AbstractHealthIndicator {
private final ApplicationAvailability applicationAvailability;
private Class<? extends AvailabilityState> stateType;
private final Map<AvailabilityState, Status> statusMappings = new HashMap<>();
/**
* Create a new {@link AvailabilityStateHealthIndicator} instance.
* @param <S> the availability state type
* @param applicationAvailability the application availability
* @param stateType the availability state type
* @param statusMappings consumer used to setup the status mappings
*/
public <S extends AvailabilityState> AvailabilityStateHealthIndicator(
ApplicationAvailability applicationAvailability, Class<S> stateType,
Consumer<StatusMappings<S>> statusMappings) {
Assert.notNull(applicationAvailability, "ApplicationAvailability must not be null");
Assert.notNull(stateType, "StateType must not be null");
Assert.notNull(statusMappings, "StatusMappings must not be null");
this.applicationAvailability = applicationAvailability;
this.stateType = stateType;
statusMappings.accept(this.statusMappings::put);
assertAllEnumsMapped(stateType);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private <S extends AvailabilityState> void assertAllEnumsMapped(Class<S> stateType) {
if (!this.statusMappings.containsKey(null) && Enum.class.isAssignableFrom(stateType)) {
EnumSet elements = EnumSet.allOf((Class) stateType);
for (Object element : elements) {
Assert.isTrue(this.statusMappings.containsKey(element), "StatusMappings does not include " + element);
}
}
}
@Override
protected void doHealthCheck(Builder builder) throws Exception {
AvailabilityState state = getState(this.applicationAvailability);
Status status = this.statusMappings.get(state);
if (status == null) {
status = this.statusMappings.get(null);
}
Assert.state(status != null, "No mapping provided for " + state);
builder.status(status);
}
/**
* Return the current availability state. Subclasses can override this method if a
* different retrieval mechanism is needed.
* @param applicationAvailability the application availability
* @return the current availability state
*/
protected AvailabilityState getState(ApplicationAvailability applicationAvailability) {
return applicationAvailability.getState(this.stateType);
}
/**
* Callback used to add status mappings.
*
* @param <S> the availability state type
*/
public interface StatusMappings<S extends AvailabilityState> {
/**
* Add the status that should be used if no explicit mapping is defined.
* @param status the default status
*/
default void addDefaultStatus(Status status) {
add(null, status);
}
/**
* Add a new status mapping .
* @param availabilityState the availability state
* @param status the mapped status
*/
void add(S availabilityState, Status status);
}
}

@ -16,10 +16,10 @@
package org.springframework.boot.actuate.availability;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.boot.availability.LivenessState;
/**
@ -28,22 +28,18 @@ import org.springframework.boot.availability.LivenessState;
* @author Brian Clozel
* @since 2.3.0
*/
public class LivenessProbeHealthIndicator extends AbstractHealthIndicator {
public class LivenessStateHealthIndicator extends AvailabilityStateHealthIndicator {
private final ApplicationAvailabilityProvider applicationAvailabilityProvider;
public LivenessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) {
this.applicationAvailabilityProvider = applicationAvailabilityProvider;
public LivenessStateHealthIndicator(ApplicationAvailability availability) {
super(availability, LivenessState.class, (statusMappings) -> {
statusMappings.add(LivenessState.CORRECT, Status.UP);
statusMappings.add(LivenessState.BROKEN, Status.DOWN);
});
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (LivenessState.LIVE.equals(this.applicationAvailabilityProvider.getLivenessState())) {
builder.up();
}
else {
builder.down();
}
protected AvailabilityState getState(ApplicationAvailability applicationAvailability) {
return applicationAvailability.getLivenessState();
}
}

@ -16,34 +16,32 @@
package org.springframework.boot.actuate.availability;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
/**
* A {@link HealthIndicator} that checks the {@link ReadinessState} of the application.
* A {@link HealthIndicator} that checks the {@link LivenessState} of the application.
*
* @author Brian Clozel
* @author Phillip Webb
* @since 2.3.0
*/
public class ReadinessProbeHealthIndicator extends AbstractHealthIndicator {
public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {
private final ApplicationAvailabilityProvider applicationAvailabilityProvider;
public ReadinessProbeHealthIndicator(ApplicationAvailabilityProvider applicationAvailabilityProvider) {
this.applicationAvailabilityProvider = applicationAvailabilityProvider;
public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
super(availability, ReadinessState.class, (statusMappings) -> {
statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
});
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (ReadinessState.READY.equals(this.applicationAvailabilityProvider.getReadinessState())) {
builder.up();
}
else {
builder.outOfService();
}
protected AvailabilityState getState(ApplicationAvailability applicationAvailability) {
return applicationAvailability.getReadinessState();
}
}

@ -23,7 +23,6 @@ import org.springframework.boot.actuate.endpoint.SecurityContext;
* by the {@link HealthEndpoint}.
*
* @author Phillip Webb
* @author Brian Clozel
* @since 2.2.0
*/
public interface HealthEndpointGroup {
@ -63,27 +62,4 @@ public interface HealthEndpointGroup {
*/
HttpCodeStatusMapper getHttpCodeStatusMapper();
/**
* Options for showing items in responses from the {@link HealthEndpointGroup} web
* extensions.
*/
enum Show {
/**
* Never show the item in the response.
*/
NEVER,
/**
* Show the item in the response when accessed by an authorized user.
*/
WHEN_AUTHORIZED,
/**
* Always show the item in the response.
*/
ALWAYS
}
}

@ -1,84 +0,0 @@
/*
* Copyright 2012-2020 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 org.springframework.boot.actuate.health.HealthEndpointGroup.Show;
/**
* A configurer for customizing an {@link HealthEndpointGroup} being built.
*
* @author Brian Clozel
* @since 2.3.0
*/
public interface HealthEndpointGroupConfigurer {
/**
* Configure the indicator endpoint ids to include in this group.
* @param indicators the indicator endpoint ids
* @return the configurer instance
*/
HealthEndpointGroupConfigurer include(String... indicators);
/**
* Configure the indicator endpoint ids to exclude from this group.
* @param indicators the indicator endpoint ids
* @return the configurer instance
*/
HealthEndpointGroupConfigurer exclude(String... indicators);
/**
* Configure the {@link StatusAggregator} to use for this group.
* <p>
* If none set, this will default to the globally configured {@link StatusAggregator}.
* @param statusAggregator the status aggregator
* @return the configurer instance
*/
HealthEndpointGroupConfigurer statusAggregator(StatusAggregator statusAggregator);
/**
* Configure the {@link HttpCodeStatusMapper} to use for this group.
* <p>
* If none set, this will default to the globally configured
* {@link HttpCodeStatusMapper}.
* @param httpCodeStatusMapper the status code mapper
* @return the configurer instance
*/
HealthEndpointGroupConfigurer httpCodeStatusMapper(HttpCodeStatusMapper httpCodeStatusMapper);
/**
* Configure the {@link Show visibility option} for showing components of this group.
* @param showComponents the components visibility
* @return the configurer instance
*/
HealthEndpointGroupConfigurer showComponents(Show showComponents);
/**
* Configure the {@link Show visibility option} for showing details of this group.
* @param showDetails the details visibility
* @return the configurer instance
*/
HealthEndpointGroupConfigurer showDetails(Show showDetails);
/**
* Configure roles used to determine whether or not a user is authorized to be shown
* details.
* @param roles the roles
* @return the configurer instance
*/
HealthEndpointGroupConfigurer roles(String... roles);
}

@ -17,19 +17,22 @@
package org.springframework.boot.actuate.health;
/**
* Callback interface that can be used to customize a
* {@link HealthEndpointGroupsRegistry}.
* Hook that allows for custom modification of {@link HealthEndpointGroups} &mdash; for
* example, automatically adding additional auto-configured groups.
*
* @author Phillip Webb
* @author Brian Clozel
* @since 2.3.0
*/
@FunctionalInterface
public interface HealthEndpointGroupsRegistryCustomizer {
public interface HealthEndpointGroupsPostProcessor {
/**
* Callback to customize a {@link HealthEndpointGroupsRegistry} instance.
* @param healthEndpointGroupsRegistry the registry to customize
* Post-process the given {@link HealthEndpointGroups} instance.
* @param groups the existing groups instance
* @return a post-processed groups instance, or the original instance if not
* post-processing was required
*/
void customize(HealthEndpointGroupsRegistry healthEndpointGroupsRegistry);
HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups);
}

@ -1,50 +0,0 @@
/*
* Copyright 2012-2020 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.function.Consumer;
/**
* Builder for an {@link HealthEndpointGroups} immutable instance.
*
* @author Brian Clozel
* @since 2.3.0
*/
public interface HealthEndpointGroupsRegistry extends HealthEndpointGroups {
/**
* Add a new {@link HealthEndpointGroup}.
* @param groupName the name of the group to add
* @param builder the group to add
* @return the builder instance
*/
HealthEndpointGroupsRegistry add(String groupName, Consumer<HealthEndpointGroupConfigurer> builder);
/**
* Remove an existing {@link HealthEndpointGroup}.
* @param groupName the name of the group to remove
* @return the builder instance
*/
HealthEndpointGroupsRegistry remove(String groupName);
/**
* Build an immutable {@link HealthEndpointGroups}.
* @return the {@link HealthEndpointGroups}
*/
HealthEndpointGroups toGroups();
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -26,6 +26,12 @@ package org.springframework.boot.actuate.health;
@FunctionalInterface
public interface HttpCodeStatusMapper {
/**
* A {@link HttpCodeStatusMapper} instance using default mappings.
* @since 2.3.0
*/
HttpCodeStatusMapper DEFAULT = new SimpleHttpCodeStatusMapper();
/**
* Return the HTTP status code that corresponds to the given {@link Status health
* status}.

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -32,6 +32,12 @@ import java.util.Set;
@FunctionalInterface
public interface StatusAggregator {
/**
* A {@link StatusAggregator} instance using default ordering rules.
* @since 2.3.0
*/
StatusAggregator DEFAULT = new SimpleStatusAggregator();
/**
* Return the aggregate status for the given set of statuses.
* @param statuses the statuses to aggregate

@ -1,50 +0,0 @@
/*
* Copyright 2012-2020 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.kubernetes;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer;
/**
* {@link HealthEndpointGroupsRegistryCustomizer} that registers {@code "liveness"} and
* {@code "readiness"} {@link HealthEndpointGroup groups} if they don't exist already.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class ProbesHealthEndpointGroupsRegistrar implements HealthEndpointGroupsRegistryCustomizer {
private static final String LIVENESS_GROUP_NAME = "liveness";
private static final String READINESS_GROUP_NAME = "readiness";
private static final String LIVENESS_PROBE_INDICATOR = "livenessProbe";
private static final String READINESS_PROBE_INDICATOR = "readinessProbe";
@Override
public void customize(HealthEndpointGroupsRegistry registry) {
if (registry.get(LIVENESS_GROUP_NAME) == null) {
registry.add(LIVENESS_GROUP_NAME, (configurer) -> configurer.include(LIVENESS_PROBE_INDICATOR));
}
if (registry.get(READINESS_GROUP_NAME) == null) {
registry.add(READINESS_GROUP_NAME, (configurer) -> configurer.include(READINESS_PROBE_INDICATOR));
}
}
}

@ -0,0 +1,118 @@
/*
* Copyright 2012-2020 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.availability;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.boot.availability.LivenessState;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link AvailabilityStateHealthIndicator}.
*
* @author Phillip Webb
*/
class AvailabilityStateHealthIndicatorTests {
@Mock
private ApplicationAvailability applicationAvailability;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
void createWhenApplicationAvailabilityIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new AvailabilityStateHealthIndicator(null, LivenessState.class, (statusMappings) -> {
})).withMessage("ApplicationAvailability must not be null");
}
@Test
void createWhenStateTypeIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(
() -> new AvailabilityStateHealthIndicator(this.applicationAvailability, null, (statusMappings) -> {
})).withMessage("StateType must not be null");
}
@Test
void createWhenStatusMappingIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(
() -> new AvailabilityStateHealthIndicator(this.applicationAvailability, LivenessState.class, null))
.withMessage("StatusMappings must not be null");
}
@Test
void createWhenStatusMappingDoesNotCoverAllEnumsThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new AvailabilityStateHealthIndicator(this.applicationAvailability,
LivenessState.class, (statusMappings) -> statusMappings.add(LivenessState.CORRECT, Status.UP)))
.withMessage("StatusMappings does not include BROKEN");
}
@Test
void healthReturnsMappedStatus() {
AvailabilityStateHealthIndicator indicator = new AvailabilityStateHealthIndicator(this.applicationAvailability,
LivenessState.class, (statusMappings) -> {
statusMappings.add(LivenessState.CORRECT, Status.UP);
statusMappings.add(LivenessState.BROKEN, Status.DOWN);
});
given(this.applicationAvailability.getState(LivenessState.class)).willReturn(LivenessState.BROKEN);
assertThat(indicator.getHealth(false).getStatus()).isEqualTo(Status.DOWN);
}
@Test
void healthReturnsDefaultStatus() {
AvailabilityStateHealthIndicator indicator = new AvailabilityStateHealthIndicator(this.applicationAvailability,
LivenessState.class, (statusMappings) -> {
statusMappings.add(LivenessState.CORRECT, Status.UP);
statusMappings.addDefaultStatus(Status.UNKNOWN);
});
given(this.applicationAvailability.getState(LivenessState.class)).willReturn(LivenessState.BROKEN);
assertThat(indicator.getHealth(false).getStatus()).isEqualTo(Status.UNKNOWN);
}
@Test
void healthWhenNotEnumReturnsMappedStatus() {
AvailabilityStateHealthIndicator indicator = new AvailabilityStateHealthIndicator(this.applicationAvailability,
TestAvailabilityState.class, (statusMappings) -> {
statusMappings.add(TestAvailabilityState.ONE, Status.UP);
statusMappings.addDefaultStatus(Status.DOWN);
});
given(this.applicationAvailability.getState(TestAvailabilityState.class)).willReturn(TestAvailabilityState.TWO);
assertThat(indicator.getHealth(false).getStatus()).isEqualTo(Status.DOWN);
}
static class TestAvailabilityState implements AvailabilityState {
static final TestAvailabilityState ONE = new TestAvailabilityState();
static final TestAvailabilityState TWO = new TestAvailabilityState();
}
}

@ -20,39 +20,39 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.LivenessState;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LivenessProbeHealthIndicator}
* Tests for {@link LivenessStateHealthIndicator}
*
* @author Brian Clozel
*/
class LivenessProbeHealthIndicatorTests {
class LivenessStateHealthIndicatorTests {
private ApplicationAvailabilityProvider stateProvider;
private ApplicationAvailability availability;
private LivenessProbeHealthIndicator healthIndicator;
private LivenessStateHealthIndicator healthIndicator;
@BeforeEach
void setUp() {
this.stateProvider = mock(ApplicationAvailabilityProvider.class);
this.healthIndicator = new LivenessProbeHealthIndicator(this.stateProvider);
this.availability = mock(ApplicationAvailability.class);
this.healthIndicator = new LivenessStateHealthIndicator(this.availability);
}
@Test
void livenessIsLive() {
when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.LIVE);
given(this.availability.getLivenessState()).willReturn(LivenessState.CORRECT);
assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP);
}
@Test
void livenessIsBroken() {
when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.BROKEN);
given(this.availability.getLivenessState()).willReturn(LivenessState.BROKEN);
assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.DOWN);
}

@ -20,39 +20,39 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.ReadinessState;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.when;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ReadinessProbeHealthIndicator}
* Tests for {@link ReadinessStateHealthIndicator}
*
* @author Brian Clozel
*/
class ReadinessProbeHealthIndicatorTests {
class ReadinessStateHealthIndicatorTests {
private ApplicationAvailabilityProvider stateProvider;
private ApplicationAvailability availability;
private ReadinessProbeHealthIndicator healthIndicator;
private ReadinessStateHealthIndicator healthIndicator;
@BeforeEach
void setUp() {
this.stateProvider = mock(ApplicationAvailabilityProvider.class);
this.healthIndicator = new ReadinessProbeHealthIndicator(this.stateProvider);
this.availability = mock(ApplicationAvailability.class);
this.healthIndicator = new ReadinessStateHealthIndicator(this.availability);
}
@Test
void readinessIsReady() {
when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.READY);
given(this.availability.getReadinessState()).willReturn(ReadinessState.ACCEPTING_TRAFFIC);
assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP);
}
@Test
void readinessIsUnready() {
when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.UNREADY);
given(this.availability.getReadinessState()).willReturn(ReadinessState.REFUSING_TRAFFIC);
assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
}

@ -1,124 +0,0 @@
/*
* Copyright 2012-2020 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.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* Test implementation for {@link HealthEndpointGroupsRegistry}
*
* @author Brian Clozel
*/
public class TestHealthEndpointGroupsRegistry implements HealthEndpointGroupsRegistry {
private TestHealthEndpointGroup primary = new TestHealthEndpointGroup();
private Map<String, TestHealthEndpointGroup> groups = new HashMap<>();
@Override
public HealthEndpointGroupsRegistry add(String groupName, Consumer<HealthEndpointGroupConfigurer> builder) {
TestHealthEndpointGroupConfigurer configurer = new TestHealthEndpointGroupConfigurer();
builder.accept(configurer);
this.groups.put(groupName, configurer.toHealthEndpointGroup());
return this;
}
@Override
public HealthEndpointGroupsRegistry remove(String groupName) {
this.groups.remove(groupName);
return this;
}
@Override
public HealthEndpointGroups toGroups() {
return this;
}
@Override
public HealthEndpointGroup getPrimary() {
return this.primary;
}
@Override
public Set<String> getNames() {
return this.groups.keySet();
}
@Override
public HealthEndpointGroup get(String name) {
return this.groups.get(name);
}
class TestHealthEndpointGroupConfigurer implements HealthEndpointGroupConfigurer {
private Predicate<String> predicate = (name) -> true;
@Override
public HealthEndpointGroupConfigurer include(String... indicators) {
Predicate<String> included = Arrays.asList(indicators).stream()
.map((group) -> (Predicate<String>) (s) -> s.equals(group)).reduce(Predicate::or)
.orElse((s) -> true);
this.predicate = this.predicate.and(included);
return this;
}
@Override
public HealthEndpointGroupConfigurer exclude(String... indicators) {
Predicate<String> excluded = Arrays.asList(indicators).stream()
.map((group) -> (Predicate<String>) (s) -> !s.equals(group)).reduce(Predicate::or)
.orElse((s) -> true);
this.predicate = this.predicate.and(excluded);
return this;
}
@Override
public HealthEndpointGroupConfigurer statusAggregator(StatusAggregator statusAggregator) {
throw new UnsupportedOperationException();
}
@Override
public HealthEndpointGroupConfigurer httpCodeStatusMapper(HttpCodeStatusMapper httpCodeStatusMapper) {
throw new UnsupportedOperationException();
}
@Override
public HealthEndpointGroupConfigurer showComponents(HealthEndpointGroup.Show showComponents) {
throw new UnsupportedOperationException();
}
@Override
public HealthEndpointGroupConfigurer showDetails(HealthEndpointGroup.Show showDetails) {
throw new UnsupportedOperationException();
}
@Override
public HealthEndpointGroupConfigurer roles(String... roles) {
throw new UnsupportedOperationException();
}
TestHealthEndpointGroup toHealthEndpointGroup() {
return new TestHealthEndpointGroup(this.predicate);
}
}
}

@ -1,64 +0,0 @@
/*
* Copyright 2012-2020 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.kubernetes;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry;
import org.springframework.boot.actuate.health.TestHealthEndpointGroupsRegistry;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@code ProbesHealthEndpointGroupsRegistrar }
*
* @author Brian Clozel
*/
class ProbesHealthEndpointGroupsRegistrarTests {
private ProbesHealthEndpointGroupsRegistrar registrar = new ProbesHealthEndpointGroupsRegistrar();
@Test
void shouldAddKubernetesProbes() {
HealthEndpointGroupsRegistry registry = new TestHealthEndpointGroupsRegistry();
this.registrar.customize(registry);
HealthEndpointGroup liveness = registry.get("liveness");
assertThat(liveness).isNotNull();
assertThat(liveness.isMember("livenessProbe")).isTrue();
HealthEndpointGroup readiness = registry.get("readiness");
assertThat(readiness).isNotNull();
assertThat(readiness.isMember("readinessProbe")).isTrue();
}
@Test
void shouldNotAddProbeIfGroupExists() {
HealthEndpointGroupsRegistry registry = new TestHealthEndpointGroupsRegistry();
registry.add("readiness", (configurer) -> configurer.include("test"));
this.registrar.customize(registry);
HealthEndpointGroup liveness = registry.get("liveness");
assertThat(liveness).isNotNull();
assertThat(liveness.isMember("livenessProbe")).isTrue();
HealthEndpointGroup readiness = registry.get("readiness");
assertThat(readiness).isNotNull();
assertThat(readiness.isMember("readinessProbe")).isFalse();
assertThat(readiness.isMember("test")).isTrue();
}
}

@ -16,13 +16,13 @@
package org.springframework.boot.autoconfigure.availability;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.ApplicationAvailabilityBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for
* {@link ApplicationAvailabilityProvider}.
* {@link ApplicationAvailabilityBean}.
*
* @author Brian Clozel
* @since 2.3.0
@ -31,8 +31,8 @@ import org.springframework.context.annotation.Configuration;
public class ApplicationAvailabilityAutoConfiguration {
@Bean
public ApplicationAvailabilityProvider applicationAvailabilityProvider() {
return new ApplicationAvailabilityProvider();
public ApplicationAvailabilityBean applicationAvailability() {
return new ApplicationAvailabilityBean();
}
}

@ -20,7 +20,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.availability.ApplicationAvailabilityProvider;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
@ -37,7 +37,7 @@ class ApplicationAvailabilityAutoConfigurationTests {
@Test
void providerIsPresent() {
this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailabilityProvider.class)));
this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class)));
}
}

@ -163,9 +163,9 @@ TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a bett
[[cloud-deployment-kubernetes]]
=== Kubernetes
Spring Boot auto-detects Kubernetes deployment environments by checking the environment for `"*_SERVICE_HOST"` and `"*_SERVICE_PORT"` variables.
You can override this detection with the configprop:management.health.probes.enabled[] configuration property.
You can override this detection with the configprop:spring.main.cloud-platform[] configuration property.
Spring Boot helps you to <<spring-boot-features.adoc#boot-features-application-availability-state,manage the state of your application>> and export it with <<production-ready-features.adoc#production-ready-kubernetes-probes, HTTP Kubernetes Probes using Actuator>>.
Spring Boot helps you to <<spring-boot-features.adoc#boot-features-application-availability,manage the state of your application>> and export it with <<production-ready-features.adoc#production-ready-kubernetes-probes, HTTP Kubernetes Probes using Actuator>>.

@ -679,15 +679,12 @@ The following `HealthIndicators` are auto-configured by Spring Boot when appropr
| {spring-boot-actuator-module-code}/jms/JmsHealthIndicator.java[`JmsHealthIndicator`]
| Checks that a JMS broker is up.
| {spring-boot-actuator-module-code}/kubernetes/LivenessProbeHealthIndicator.java[`LivenessProbeHealthIndicator`]
| Exposes the "Liveness" application state.
| {spring-boot-actuator-module-code}/kubernetes/ReadinessProbeHealthIndicator.java[`ReadinessProbeHealthIndicator`]
| Exposes the "Readiness" application state.
| {spring-boot-actuator-module-code}/ldap/LdapHealthIndicator.java[`LdapHealthIndicator`]
| Checks that an LDAP server is up.
| {spring-boot-actuator-module-code}/availability/LivenessStateHealthIndicator.java[`LivenessStateHealthIndicator`]
| Exposes the "Liveness" application availability state.
| {spring-boot-actuator-module-code}/mail/MailHealthIndicator.java[`MailHealthIndicator`]
| Checks that a mail server is up.
@ -703,6 +700,9 @@ The following `HealthIndicators` are auto-configured by Spring Boot when appropr
| {spring-boot-actuator-module-code}/amqp/RabbitHealthIndicator.java[`RabbitHealthIndicator`]
| Checks that a Rabbit server is up.
| {spring-boot-actuator-module-code}/availability/ReadinessStateHealthIndicator.java[`ReadinessStateHealthIndicator`]
| Exposes the "Readiness" application availability state.
| {spring-boot-actuator-module-code}/redis/RedisHealthIndicator.java[`RedisHealthIndicator`]
| Checks that a Redis server is up.
@ -876,8 +876,8 @@ TIP: You can use `@Qualifier("groupname")` if you need to register custom `Statu
Applications deployed on Kubernetes can provide information about their internal state with https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes[Container Probes].
Depending on https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[your Kubernetes configuration], the kubelet will call those probes and react to the result.
Spring Boot manages your <<spring-boot-features.adoc#boot-features-application-availability-state,Application Availability State>> out-of-the-box.
If deployed in a Kubernetes environment, Actuator will gather the "Liveness" and "Readiness" information from the `ApplicationAvailabilityProvider` and use that information in dedicated <<production-ready-health-indicators,Health Indicators>>: `LivenessProbeHealthIndicator` and `ReadinessProbeHealthIndicator`.
Spring Boot manages your <<spring-boot-features.adoc#boot-features-application-availability,Application Availability State>> out-of-the-box.
If deployed in a Kubernetes environment, actuator will gather the "Liveness" and "Readiness" information from the `ApplicationAvailability` interface and use that information in dedicated <<production-ready-health-indicators,Health Indicators>>: `LivenessStateHealthIndicator` and `ReadinessStateHealthIndicator`.
These indicators will be shown on the global health endpoint (`"/actuator/health"`).
They will also be exposed as separate HTTP Probes using <<production-ready-health-groups, Health Groups>>: `"/actuator/health/liveness"` and `"/actuator/health/readiness"`.
@ -904,33 +904,37 @@ NOTE: If an application takes longer to start than the configured liveness perio
The `"startupProbe"` is not necessarily needed here as the `"readinessProbe"` fails until all startup tasks are done, see <<production-ready-features.adoc#production-ready-kubernetes-probes-lifecycle,how Probes behave during the application lifecycle>>.
WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application.
In this case, a Probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections).
In this case, a probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections).
[[production-ready-kubernetes-probes-external-state]]
==== Checking external state with Kubernetes Probes
Actuator configures the "liveness" and "readiness" Probes as Health Groups; this means that all the <<production-ready-health-groups, Health Groups features>> are available for them.
Actuator configures the "liveness" and "readiness" probes as Health Groups; this means that all the <<production-ready-health-groups, Health Groups features>> are available for them.
You can, for example, configure additional Health Indicators:
[source,properties,indent=0,configprops]
----
management.endpoint.health.group.readiness.include=readinessProbe,customCheck
management.endpoint.health.group.readiness.include=readinessState,customCheck
----
By default, Spring Boot does not add other Health Indicators to these groups.
The "liveness" Probe should not depend on health checks for external systems.
If the <<spring-boot-features.adoc#boot-features-application-availability-liveness,Liveness State of an application>> is broken, Kubernetes will try to solve that problem by restarting the application instance.
The "`liveness`" Probe should not depend on health checks for external systems.
If the <<spring-boot-features.adoc#boot-features-application-availability-liveness-state,Liveness State of an application>> is broken, Kubernetes will try to solve that problem by restarting the application instance.
This means that if an external system fails (e.g. a database, a Web API, an external cache), Kubernetes might restart all application instances and create cascading failures.
As for the "readiness" Probe, the choice of checking external systems must be made carefully by the application developers, i.e. Spring Boot does not include any additional health checks in the readiness probe.
If the <<spring-boot-features.adoc#boot-features-application-availability-readiness,Readiness State of an application instance>> is unready, Kubernetes will not route traffic to that instance.
As for the "`readiness`" Probe, the choice of checking external systems must be made carefully by the application developers, i.e. Spring Boot does not include any additional health checks in the readiness probe.
If the <<spring-boot-features.adoc#boot-features-application-availability-readiness-state,Readiness State of an application instance>> is unready, Kubernetes will not route traffic to that instance.
Some external systems might not be shared by application instances, in which case they could quite naturally be included in a readiness probe.
Other external systems might not be essential to the application (the application could have circuit breakers and fallbacks), in which case they definitely should not be included.
Unfortunately, an external system that is shared by all application instances is common, and you have to make a judgement call: include it in the readiness probe and expect that the application is taken out of service when the external service is down, or leave it out and deal with failures higher up the stack, e.g. using a circuit breaker in the caller.
NOTE: If all instances of an application are unready, a Kubernetes Service with `type=ClusterIP` or `NodePort` will not accept any incoming connections. There is no HTTP error response (503 etc.) since there is no connection. A Service with `type=LoadBalancer` might or might not accept connections, depending on the provider. A Service that has an explicit https://kubernetes.io/docs/concepts/services-networking/ingress/[Ingress] will also respond in a way that depends on the implementation - the ingress service itself will have to decide how to handle the "connection refused" from downstream. HTTP 503 is quite likely in the case of both load balancer and ingress.
NOTE: If all instances of an application are unready, a Kubernetes Service with `type=ClusterIP` or `NodePort` will not accept any incoming connections.
There is no HTTP error response (503 etc.) since there is no connection.
A Service with `type=LoadBalancer` might or might not accept connections, depending on the provider.
A Service that has an explicit https://kubernetes.io/docs/concepts/services-networking/ingress/[Ingress] will also respond in a way that depends on the implementation - the ingress service itself will have to decide how to handle the "connection refused" from downstream.
HTTP 503 is quite likely in the case of both load balancer and ingress.
Also, if an application is using Kubernetes https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/[autoscaling] it may react differently to applications being taken out of the load-balancer, depending on its autoscaler configuration.
@ -948,18 +952,18 @@ When a Spring Boot application starts:
|Application startup phase |Liveness State |Readiness State |Notes
|Starting
|broken
|unready
|`BROKEN`
|`REFUSING_TRAFFIC`
|Kubernetes checks the "liveness" Probe and restarts the application if it takes too long.
|Started
|live
|unready
|`CORRECT`
|`REFUSING_TRAFFIC`
|The application context is refreshed. The application performs startup tasks and does not receive traffic yet.
|Ready
|live
|ready
|`CORRECT`
|`ACCEPTING_TRAFFIC`
|Startup tasks are finished. The application is receiving traffic.
|===

@ -193,31 +193,34 @@ See the {spring-boot-module-api}/builder/SpringApplicationBuilder.html[`SpringAp
[[boot-features-application-availability-state]]
=== Application Availability State
When deployed on plaftorms, applications can provide information about their availability to the platform using infrastructure like https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes].
Spring Boot manages this application state with the `ApplicationAvailabilityProvider` and makes it available to application components and the platform itself.
[[boot-features-application-availability]]
=== Application Availability
When deployed on platforms, applications can provide information about their availability to the platform using infrastructure such as https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes].
Spring Boot includes out-of-the box support for the commonly used "`liveness`" and "`readiness`" availability states.
If you are using Spring Boot's "`actuator`" support then these states are exposed as health endpoint groups.
In addition, you can also obtain availability states by injecting the `ApplicationAvailability` interface into your own beans.
[[boot-features-application-availability-liveness]]
[[boot-features-application-availability-liveness-state]]
==== Liveness State
The "Liveness" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing.
An invalid "Liveness" state means that the application is in a broken state and cannot recover from it; the infrastructure should then restart the application to mitigate that problem.
The "`Liveness`" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing.
A broken "`Liveness`" state means that the application is in a state that it cannot recover from, and the infrastructure should restart the application.
NOTE: In general, the "Liveness" state should not be based on external checks, such as <<production-ready-features.adoc#production-ready-health, Health checks>>.
If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform.
The internal state of Spring Boot applications is mostly represented by the Spring application context.
The internal state of Spring Boot applications is mostly represented by the Spring `ApplicationContext`.
If the application context has started successfully, Spring Boot assumes that the application is in a valid state.
An application is considered live as soon as the context has been refreshed, see <<boot-features-application-events-and-listeners, Spring Boot application lifecycle and related Application Events>>.
[[boot-features-application-availability-readiness]]
[[boot-features-application-availability-readiness-state]]
==== Readiness State
The "Readiness" state of an application tells whether the application is ready to handle traffic.
A failing "Readiness" state tells the platform that it should not route traffic to the application for now.
The "`Readiness`" state of an application tells whether the application is ready to handle traffic.
A failing "`Readiness`" state tells the platform that it should not route traffic to the application for now.
This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic.
An application is considered ready as soon as application and command-line runners have been called, see <<boot-features-application-events-and-listeners, Spring Boot application lifecycle and related Application Events>>.
@ -226,9 +229,9 @@ TIP: Tasks expected to run during startup should be executed by `CommandLineRunn
[[boot-features-managing-application-availability]]
[[boot-features-application-availability-managing]]
==== Managing the Application Availability State
Application components can retrieve the current availability state at any time, by injecting the `ApplicationAvailabilityProvider` and calling methods on it.
Application components can retrieve the current availability state at any time, by injecting the `ApplicationAvailability` interface and calling methods on it.
More often, applications will want to listen to state updates or update the state of the application.
For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file:
@ -236,15 +239,15 @@ For example, we can export the "Readiness" state of the application to a file so
[source,java,indent=0]
----
@Component
public class ReadinessStateExporter implements ApplicationListener<ReadinessStateChangedEvent> {
public class ReadinessStateExporter {
@Override
public void onApplicationEvent(ReadinessStateChangedEvent event) {
switch (event.getReadinessState().getStatus()) {
case READY:
@EventListener
public void onStateChange(AvailabilityChangeEvent<ReadinessState> event) {
switch (event.getState()) {
case ACCEPTING_TRAFFIC:
// create file /tmp/healthy
break;
case UNREADY:
case REFUSING_TRAFFIC:
// remove file /tmp/healthy
break;
}
@ -271,7 +274,7 @@ We can also update the state of the application, when the application breaks and
//...
}
catch (CacheCompletelyBrokenException ex) {
this.eventPublisher.publishEvent(LivenessStateChangedEvent.broken(ex));
AvailabilityChangeEvent.publish(this.eventPublisher, ex, LivenessState.BROKEN);
}
}
@ -282,6 +285,7 @@ Spring Boot provides <<production-ready-features.adoc#production-ready-kubernete
You can get more guidance about <<deployment.adoc#cloud-deployment-kubernetes,deploying Spring Boot applications on Kubernetes in the dedicated section>>.
[[boot-features-application-events-and-listeners]]
=== Application Events and Listeners
In addition to the usual Spring Framework events, such as {spring-framework-api}/context/event/ContextRefreshedEvent.html[`ContextRefreshedEvent`], a `SpringApplication` sends some additional application events.
@ -307,9 +311,9 @@ Application events are sent in the following order, as your application runs:
. An `ApplicationContextInitializedEvent` is sent when the `ApplicationContext` is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded.
. An `ApplicationPreparedEvent` is sent just before the refresh is started but after bean definitions have been loaded.
. An `ApplicationStartedEvent` is sent after the context has been refreshed but before any application and command-line runners have been called.
. An `LivenessStateChangedEvent` is sent right after to indicate that the application is considered as live.
. An `AvailabilityChangeEvent` is sent right after with `LivenessState.CORRECT` to indicate that the application is considered as live.
. An `ApplicationReadyEvent` is sent after any application and command-line runners have been called.
. An `ReadinessStateChangedEvent` is sent right after to indicate that the application is ready to service requests.
. An `LivenessState` is sent right after with `ReadinessState.ACCEPTING_TRAFFIC` to indicate that the application is ready to service requests.
. An `ApplicationFailedEvent` is sent if there is an exception on startup.
The above list only includes ``SpringApplicationEvent``s that are tied to a `SpringApplication`.

@ -0,0 +1,81 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.context.ApplicationContext;
/**
* Provides {@link AvailabilityState availability state} information for the application.
* <p>
* Components can inject this class to get the current state information. To update the
* state of the application an {@link AvailabilityChangeEvent} should be
* {@link ApplicationContext#publishEvent published} to the application context with
* directly or via {@link AvailabilityChangeEvent#publish}.
*
* @author Brian Clozel
* @author Phillip Webb
* @since 2.3.0
*/
public interface ApplicationAvailability {
/**
* Return the {@link LivenessState} of the application.
* @return the liveness state
*/
default LivenessState getLivenessState() {
return getState(LivenessState.class, LivenessState.BROKEN);
}
/**
* Return the {@link ReadinessState} of the application.
* @return the readiness state
*/
default ReadinessState getReadinessState() {
return getState(ReadinessState.class, ReadinessState.REFUSING_TRAFFIC);
}
/**
* Return {@link AvailabilityState} information for the application.
* @param <S> the state type
* @param stateType the state type
* @param defaultState the default state to return if no event of the given type has
* been published yet (must not be {@code null}.
* @return the readiness state
* @see #getState(Class)
*/
<S extends AvailabilityState> S getState(Class<S> stateType, S defaultState);
/**
* Return {@link AvailabilityState} information for the application.
* @param <S> the state type
* @param stateType the state type
* @return the readiness state or {@code null} if no event of the given type has been
* published yet
* @see #getState(Class, AvailabilityState)
*/
<S extends AvailabilityState> S getState(Class<S> stateType);
/**
* Return the last {@link AvailabilityChangeEvent} received for a given state type.
* @param <S> the state type
* @param stateType the state type
* @return the readiness state or {@code null} if no event of the given type has been
* published yet
*/
<S extends AvailabilityState> AvailabilityChangeEvent<S> getLastChangeEvent(Class<S> stateType);
}

@ -0,0 +1,73 @@
/*
* Copyright 2012-2020 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.availability;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.ApplicationListener;
import org.springframework.util.Assert;
/**
* Bean that provides an {@link ApplicationAvailability} implementation by listening for
* {@link AvailabilityChangeEvent change events}.
*
* @author Brian Clozel
* @author Phillip Webb
* @since 2.3.0
* @see ApplicationAvailability
*/
public class ApplicationAvailabilityBean
implements ApplicationAvailability, ApplicationListener<AvailabilityChangeEvent<?>> {
private final Map<Class<? extends AvailabilityState>, AvailabilityChangeEvent<?>> events = new HashMap<>();
@Override
public <S extends AvailabilityState> S getState(Class<S> stateType, S defaultState) {
Assert.notNull(stateType, "StateType must not be null");
Assert.notNull(defaultState, "DefaultState must not be null");
S state = getState(stateType);
return (state != null) ? state : defaultState;
}
@Override
public <S extends AvailabilityState> S getState(Class<S> stateType) {
AvailabilityChangeEvent<S> event = getLastChangeEvent(stateType);
return (event != null) ? event.getState() : null;
}
@Override
@SuppressWarnings("unchecked")
public <S extends AvailabilityState> AvailabilityChangeEvent<S> getLastChangeEvent(Class<S> stateType) {
return (AvailabilityChangeEvent<S>) this.events.get(stateType);
}
@Override
public void onApplicationEvent(AvailabilityChangeEvent<?> event) {
Class<? extends AvailabilityState> stateType = getStateType(event.getState());
this.events.put(stateType, event);
}
@SuppressWarnings("unchecked")
private Class<? extends AvailabilityState> getStateType(AvailabilityState state) {
if (state instanceof Enum) {
return (Class<? extends AvailabilityState>) ((Enum<?>) state).getDeclaringClass();
}
return state.getClass();
}
}

@ -1,88 +0,0 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.util.Assert;
/**
* Holds the availability state of the application.
* <p>
* Other application components can get the current state information from the
* {@code ApplicationAvailabilityProvider}, or publish application evens such as
* {@link ReadinessStateChangedEvent} and {@link LivenessStateChangedEvent} to update the
* state of the application.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class ApplicationAvailabilityProvider implements ApplicationListener<ApplicationEvent> {
private LivenessState livenessState;
private ReadinessState readinessState;
/**
* Create a new {@link ApplicationAvailabilityProvider} instance with
* {@link LivenessState#BROKEN} and {@link ReadinessState#UNREADY}.
*/
public ApplicationAvailabilityProvider() {
this(LivenessState.BROKEN, ReadinessState.UNREADY);
}
/**
* Create a new {@link ApplicationAvailabilityProvider} with the given states.
* @param livenessState the liveness state
* @param readinessState the readiness state
*/
public ApplicationAvailabilityProvider(LivenessState livenessState, ReadinessState readinessState) {
Assert.notNull(livenessState, "LivenessState must not be null");
Assert.notNull(readinessState, "ReadinessState must not be null");
this.livenessState = livenessState;
this.readinessState = readinessState;
}
/**
* Return the {@link LivenessState} of the application.
* @return the liveness state
*/
public LivenessState getLivenessState() {
return this.livenessState;
}
/**
* Return the {@link ReadinessState} of the application.
* @return the readiness state
*/
public ReadinessState getReadinessState() {
return this.readinessState;
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof LivenessStateChangedEvent) {
LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event;
this.livenessState = livenessEvent.getLivenessState();
}
else if (event instanceof ReadinessStateChangedEvent) {
ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event;
this.readinessState = readinessEvent.getReadinessState();
}
}
}

@ -0,0 +1,84 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
/**
* {@link ApplicationEvent} sent when the {@link AvailabilityState} of the application
* changes.
* <p>
* Any application component can send such events to update the state of the application.
*
* @param <S> the availability state type
* @author Brian Clozel
* @author Phillip Webb
* @since 2.3.0
*/
public class AvailabilityChangeEvent<S extends AvailabilityState> extends ApplicationEvent {
private final S state;
/**
* Create a new {@link AvailabilityChangeEvent} instance.
* @param source the source of the event
* @param state the availability state (never {@code null})
*/
public AvailabilityChangeEvent(Object source, S state) {
super(source);
Assert.notNull(state, "State must not be null");
this.state = state;
}
/**
* Return the changed availability state.
* @return the availability state
*/
public S getState() {
return this.state;
}
/**
* Convenience method that can be used to publish an {@link AvailabilityChangeEvent}
* to the given application context.
* @param <S> the availability state type
* @param context the context used to publish the event
* @param state the changed availability state
*/
public static <S extends AvailabilityState> void publish(ApplicationContext context, S state) {
Assert.notNull(context, "Context must not be null");
publish(context, context, state);
}
/**
* Convenience method that can be used to publish an {@link AvailabilityChangeEvent}
* to the given application context.
* @param <S> the availability state type
* @param publisher the publisher used to publish the event
* @param source the source of the event
* @param state the changed availability state
*/
public static <S extends AvailabilityState> void publish(ApplicationEventPublisher publisher, Object source,
S state) {
Assert.notNull(publisher, "Publisher must not be null");
publisher.publishEvent(new AvailabilityChangeEvent<>(source, state));
}
}

@ -14,7 +14,17 @@
* limitations under the License.
*/
package org.springframework.boot.availability;
/**
* Actuator support for kubernetes-related concerns.
* Tagging interface used on {@link ApplicationAvailability} states. This interface is
* usually implemented on an {@code enum} type.
*
* @author Phillip Webb
* @since 2.3.0
* @see LivenessState
* @see ReadinessState
*/
package org.springframework.boot.actuate.kubernetes;
public interface AvailabilityState {
}

@ -26,15 +26,15 @@ package org.springframework.boot.availability;
* @author Brian Clozel
* @since 2.3.0
*/
public enum LivenessState {
public enum LivenessState implements AvailabilityState {
/**
* The application is running and its internal state is correct.
*/
LIVE,
CORRECT,
/**
* The internal state of the application is broken.
* The application is running but its internal state is broken.
*/
BROKEN

@ -1,74 +0,0 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.context.ApplicationEvent;
/**
* {@link ApplicationEvent} sent when the {@link LivenessState} of the application
* changes.
* <p>
* Any application component can send such events to update the state of the application.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class LivenessStateChangedEvent extends ApplicationEvent {
private final String cause;
LivenessStateChangedEvent(LivenessState state, String cause) {
super(state);
this.cause = cause;
}
public LivenessState getLivenessState() {
return (LivenessState) getSource();
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is
* live.
* @param cause the cause of the live internal state of the application
* @return the application event
*/
public static LivenessStateChangedEvent live(String cause) {
return new LivenessStateChangedEvent(LivenessState.LIVE, cause);
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is
* broken.
* @param cause the cause of the broken internal state of the application
* @return the application event
*/
public static LivenessStateChangedEvent broken(String cause) {
return new LivenessStateChangedEvent(LivenessState.BROKEN, cause);
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is
* broken.
* @param throwable the exception that caused the broken internal state of the
* application
* @return the application event
*/
public static LivenessStateChangedEvent broken(Throwable throwable) {
return new LivenessStateChangedEvent(LivenessState.BROKEN, throwable.getMessage());
}
}

@ -26,16 +26,16 @@ package org.springframework.boot.availability;
* @author Brian Clozel
* @since 2.3.0
*/
public enum ReadinessState {
public enum ReadinessState implements AvailabilityState {
/**
* The application is not willing to receive traffic.
* The application is ready to receive traffic.
*/
UNREADY,
ACCEPTING_TRAFFIC,
/**
* The application is ready to receive traffic.
* The application is not willing to receive traffic.
*/
READY
REFUSING_TRAFFIC
}

@ -1,58 +0,0 @@
/*
* Copyright 2012-2020 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.availability;
import org.springframework.context.ApplicationEvent;
/**
* {@link ApplicationEvent} sent when the {@link ReadinessState} of the application
* changes.
* <p>
* Any application component can send such events to update the state of the application.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class ReadinessStateChangedEvent extends ApplicationEvent {
ReadinessStateChangedEvent(ReadinessState state) {
super(state);
}
public ReadinessState getReadinessState() {
return (ReadinessState) getSource();
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is
* ready.
* @return the application event
*/
public static ReadinessStateChangedEvent ready() {
return new ReadinessStateChangedEvent(ReadinessState.READY);
}
/**
* Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is
* unready.
* @return the application event
*/
public static ReadinessStateChangedEvent unready() {
return new ReadinessStateChangedEvent(ReadinessState.UNREADY);
}
}

@ -21,8 +21,9 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.boot.availability.LivenessStateChangedEvent;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
@ -99,13 +100,13 @@ public class EventPublishingRunListener implements SpringApplicationRunListener,
@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
context.publishEvent(LivenessStateChangedEvent.live("Application started"));
AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
context.publishEvent(ReadinessStateChangedEvent.ready());
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}
@Override

@ -22,7 +22,8 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServer;
@ -149,7 +150,7 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli
@Override
protected void doClose() {
publishEvent(ReadinessStateChangedEvent.unready());
AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC);
WebServer webServer = getWebServer();
if (webServer != null) {
webServer.shutDownGracefully();

@ -37,7 +37,8 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@ -169,7 +170,7 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon
@Override
protected void doClose() {
publishEvent(ReadinessStateChangedEvent.unready());
AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC);
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.shutDownGracefully();

@ -33,6 +33,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mockito;
@ -46,8 +47,10 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
import org.springframework.boot.availability.LivenessStateChangedEvent;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationFailedEvent;
@ -108,6 +111,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.atLeastOnce;
@ -410,9 +414,10 @@ class SpringApplicationTests {
inOrder.verify(listener).onApplicationEvent(isA(ApplicationPreparedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ContextRefreshedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(LivenessStateChangedEvent.class));
inOrder.verify(listener).onApplicationEvent(argThat(isAvailabilityChangeEventWithState(LivenessState.CORRECT)));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ReadinessStateChangedEvent.class));
inOrder.verify(listener)
.onApplicationEvent(argThat(isAvailabilityChangeEventWithState(ReadinessState.ACCEPTING_TRAFFIC)));
inOrder.verifyNoMoreInteractions();
}
@ -886,7 +891,7 @@ class SpringApplicationTests {
this.context = application.run();
assertThat(events).hasAtLeastOneElementOfType(ApplicationPreparedEvent.class);
assertThat(events).hasAtLeastOneElementOfType(ContextRefreshedEvent.class);
verifyTestListenerEvents();
verifyRegisteredListenerSuccessEvents();
}
@Test
@ -899,24 +904,21 @@ class SpringApplicationTests {
this.context = application.run();
assertThat(events).hasAtLeastOneElementOfType(ApplicationPreparedEvent.class);
assertThat(events).hasAtLeastOneElementOfType(ContextRefreshedEvent.class);
verifyTestListenerEvents();
verifyRegisteredListenerSuccessEvents();
}
@SuppressWarnings("unchecked")
private void verifyTestListenerEvents() {
private void verifyRegisteredListenerSuccessEvents() {
ApplicationListener<ApplicationEvent> listener = this.context.getBean("testApplicationListener",
ApplicationListener.class);
verifyListenerEvents(listener, ContextRefreshedEvent.class, ApplicationStartedEvent.class,
LivenessStateChangedEvent.class, ApplicationReadyEvent.class, ReadinessStateChangedEvent.class);
}
@SuppressWarnings("unchecked")
private void verifyListenerEvents(ApplicationListener<ApplicationEvent> listener,
Class<? extends ApplicationEvent>... eventTypes) {
for (Class<? extends ApplicationEvent> eventType : eventTypes) {
verify(listener).onApplicationEvent(isA(eventType));
}
verifyNoMoreInteractions(listener);
InOrder inOrder = Mockito.inOrder(listener);
inOrder.verify(listener).onApplicationEvent(isA(ContextRefreshedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartedEvent.class));
inOrder.verify(listener).onApplicationEvent(argThat(isAvailabilityChangeEventWithState(LivenessState.CORRECT)));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationReadyEvent.class));
inOrder.verify(listener)
.onApplicationEvent(argThat(isAvailabilityChangeEventWithState(ReadinessState.ACCEPTING_TRAFFIC)));
inOrder.verifyNoMoreInteractions();
}
@SuppressWarnings("unchecked")
@ -926,8 +928,7 @@ class SpringApplicationTests {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.addListeners(listener);
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(application::run);
verifyListenerEvents(listener, ApplicationStartingEvent.class, ApplicationEnvironmentPreparedEvent.class,
ApplicationContextInitializedEvent.class, ApplicationPreparedEvent.class, ApplicationFailedEvent.class);
verifyRegisteredListenerFailedFromApplicationEvents(listener);
}
@SuppressWarnings("unchecked")
@ -938,8 +939,17 @@ class SpringApplicationTests {
application.setWebApplicationType(WebApplicationType.NONE);
application.addListeners(listener);
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run);
verifyListenerEvents(listener, ApplicationStartingEvent.class, ApplicationEnvironmentPreparedEvent.class,
ApplicationContextInitializedEvent.class, ApplicationPreparedEvent.class, ApplicationFailedEvent.class);
verifyRegisteredListenerFailedFromApplicationEvents(listener);
}
private void verifyRegisteredListenerFailedFromApplicationEvents(ApplicationListener<ApplicationEvent> listener) {
InOrder inOrder = Mockito.inOrder(listener);
inOrder.verify(listener).onApplicationEvent(isA(ApplicationStartingEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationEnvironmentPreparedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationContextInitializedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationPreparedEvent.class));
inOrder.verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class));
inOrder.verifyNoMoreInteractions();
}
@SuppressWarnings("unchecked")
@ -949,7 +959,8 @@ class SpringApplicationTests {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.addInitializers((applicationContext) -> applicationContext.addApplicationListener(listener));
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(application::run);
verifyListenerEvents(listener, ApplicationFailedEvent.class);
verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class));
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("unchecked")
@ -960,7 +971,17 @@ class SpringApplicationTests {
application.setWebApplicationType(WebApplicationType.NONE);
application.addInitializers((applicationContext) -> applicationContext.addApplicationListener(listener));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(application::run);
verifyListenerEvents(listener, ApplicationFailedEvent.class);
verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class));
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("unchecked")
private void verifyRegisteredListenerFailedFromContextEvents() {
ApplicationListener<ApplicationEvent> listener = this.context.getBean("testApplicationListener",
ApplicationListener.class);
InOrder inOrder = Mockito.inOrder(listener);
inOrder.verify(listener).onApplicationEvent(isA(ApplicationFailedEvent.class));
inOrder.verifyNoMoreInteractions();
}
@Test
@ -1124,6 +1145,12 @@ class SpringApplicationTests {
.getBean(AtomicInteger.class)).hasValue(1);
}
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
S state) {
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
&& ((AvailabilityChangeEvent<?>) argument).getState().equals(state);
}
private Condition<ConfigurableEnvironment> matchingPropertySource(final Class<?> propertySourceClass,
final String name) {

@ -0,0 +1,110 @@
/*
* Copyright 2012-2020 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.availability;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ApplicationAvailabilityBean}
*
* @author Brian Clozel
* @author Phillip Webb
*/
class ApplicationAvailabilityBeanTests {
private AnnotationConfigApplicationContext context;
private ApplicationAvailabilityBean availability;
@BeforeEach
void setup() {
this.context = new AnnotationConfigApplicationContext(ApplicationAvailabilityBean.class);
this.availability = this.context.getBean(ApplicationAvailabilityBean.class);
}
@Test
void getLivenessStateWhenNoEventHasBeenPublishedReturnsDefaultState() {
assertThat(this.availability.getLivenessState()).isEqualTo(LivenessState.BROKEN);
}
@Test
void getLivenessStateWhenEventHasBeenPublishedReturnsPublishedState() {
AvailabilityChangeEvent.publish(this.context, LivenessState.CORRECT);
assertThat(this.availability.getLivenessState()).isEqualTo(LivenessState.CORRECT);
}
@Test
void getReadinessStateWhenNoEventHasBeenPublishedReturnsDefaultState() {
assertThat(this.availability.getReadinessState()).isEqualTo(ReadinessState.REFUSING_TRAFFIC);
}
@Test
void getReadinessStateWhenEventHasBeenPublishedReturnsPublishedState() {
AvailabilityChangeEvent.publish(this.context, ReadinessState.ACCEPTING_TRAFFIC);
assertThat(this.availability.getReadinessState()).isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);
}
@Test
void getStateWhenNoEventHasBeenPublishedReturnsDefaultState() {
assertThat(this.availability.getState(TestState.class)).isNull();
assertThat(this.availability.getState(TestState.class, TestState.ONE)).isEqualTo(TestState.ONE);
}
@Test
void getStateWhenEventHasBeenPublishedReturnsPublishedState() {
AvailabilityChangeEvent.publish(this.context, TestState.TWO);
assertThat(this.availability.getState(TestState.class)).isEqualTo(TestState.TWO);
assertThat(this.availability.getState(TestState.class, TestState.ONE)).isEqualTo(TestState.TWO);
}
@Test
void getLastChangeEventWhenNoEventHasBeenPublishedReturnsDefaultState() {
assertThat(this.availability.getLastChangeEvent(TestState.class)).isNull();
}
@Test
void getLastChangeEventWhenEventHasBeenPublishedReturnsPublishedState() {
AvailabilityChangeEvent.publish(this.context, TestState.TWO);
assertThat(this.availability.getLastChangeEvent(TestState.class)).isNotNull();
}
enum TestState implements AvailabilityState {
ONE {
@Override
public String test() {
return "spring";
}
},
TWO {
@Override
public String test() {
return "boot";
}
};
abstract String test();
}
}

@ -1,52 +0,0 @@
/*
* Copyright 2012-2020 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.availability;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ApplicationAvailabilityProvider}
*
* @author Brian Clozel
*/
class ApplicationAvailabilityProviderTests {
@Test
void initialStateShouldBeFailures() {
ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider();
assertThat(stateProvider.getLivenessState()).isEqualTo(LivenessState.BROKEN);
assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.UNREADY);
}
@Test
void updateLivenessState() {
ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider();
LivenessState livenessState = LivenessState.LIVE;
stateProvider.onApplicationEvent(new LivenessStateChangedEvent(livenessState, "Startup complete"));
assertThat(stateProvider.getLivenessState()).isEqualTo(livenessState);
}
@Test
void updateReadiessState() {
ApplicationAvailabilityProvider stateProvider = new ApplicationAvailabilityProvider();
stateProvider.onApplicationEvent(ReadinessStateChangedEvent.ready());
assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.READY);
}
}

@ -0,0 +1,64 @@
/*
* Copyright 2012-2020 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.availability;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link AvailabilityChangeEvent}.
*
* @author Phillip Webb
*/
class AvailabilityChangeEventTests {
private Object source = new Object();
@Test
void createWhenStateIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new AvailabilityChangeEvent<>(this.source, null))
.withMessage("State must not be null");
}
@Test
void getStateReturnsState() {
LivenessState state = LivenessState.CORRECT;
AvailabilityChangeEvent<LivenessState> event = new AvailabilityChangeEvent<>(this.source, state);
assertThat(event.getState()).isEqualTo(state);
}
@Test
void publishPublishesEvent() {
ApplicationContext context = mock(ApplicationContext.class);
AvailabilityState state = LivenessState.CORRECT;
AvailabilityChangeEvent.publish(context, state);
ArgumentCaptor<ApplicationEvent> captor = ArgumentCaptor.forClass(ApplicationEvent.class);
verify(context).publishEvent(captor.capture());
AvailabilityChangeEvent<?> event = (AvailabilityChangeEvent<?>) captor.getValue();
assertThat(event.getSource()).isEqualTo(context);
assertThat(event.getState()).isEqualTo(state);
}
}

@ -26,8 +26,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.availability.LivenessStateChangedEvent;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.support.StaticApplicationContext;
@ -71,9 +70,9 @@ class EventPublishingRunListenerTests {
checkApplicationEvents(ApplicationPreparedEvent.class);
context.refresh();
this.runListener.started(context);
checkApplicationEvents(ApplicationStartedEvent.class, LivenessStateChangedEvent.class);
checkApplicationEvents(ApplicationStartedEvent.class, AvailabilityChangeEvent.class);
this.runListener.running(context);
checkApplicationEvents(ApplicationReadyEvent.class, ReadinessStateChangedEvent.class);
checkApplicationEvents(ApplicationReadyEvent.class, AvailabilityChangeEvent.class);
}
void checkApplicationEvents(Class<?>... eventClasses) {

@ -49,7 +49,7 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.availability.ReadinessStateChangedEvent;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
@ -178,7 +178,7 @@ class ServletWebServerApplicationContextTests {
this.context.refresh();
this.context.addApplicationListener(listener);
this.context.close();
assertThat(listener.receivedEvents()).hasSize(2).extracting("class").contains(ReadinessStateChangedEvent.class,
assertThat(listener.receivedEvents()).hasSize(2).extracting("class").contains(AvailabilityChangeEvent.class,
ContextClosedEvent.class);
}

Loading…
Cancel
Save