diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java index cfea36262df6..6d9b45c56ef1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java @@ -73,6 +73,12 @@ public HealthIndicatorRegistry healthIndicatorRegistry( return HealthIndicatorRegistryBeans.get(applicationContext); } + @Bean + public StickyHealthIndicatorDecoratorBeanPostProcessor stickyHealthIndicatorDecoratorBeanPostProcessor( + HealthIndicatorProperties properties) { + return new StickyHealthIndicatorDecoratorBeanPostProcessor(properties.getSticky()); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Flux.class) static class ReactiveHealthIndicatorConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java index 989d61f241a8..11b5da7b4cfd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java @@ -17,8 +17,10 @@ package org.springframework.boot.actuate.autoconfigure.health; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -42,6 +44,11 @@ public class HealthIndicatorProperties { */ private final Map httpMapping = new HashMap<>(); + /** + * Health indicators that must always stay UP in case they were UP at least once. + */ + private Set sticky = new HashSet<>(); + public List getOrder() { return this.order; } @@ -56,4 +63,12 @@ public Map getHttpMapping() { return this.httpMapping; } + public Set getSticky() { + return sticky; + } + + public void setSticky(Set sticky) { + this.sticky = sticky; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/StickyHealthIndicatorDecoratorBeanPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/StickyHealthIndicatorDecoratorBeanPostProcessor.java new file mode 100644 index 000000000000..b5c90a3ce46d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/StickyHealthIndicatorDecoratorBeanPostProcessor.java @@ -0,0 +1,51 @@ +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorNameFactory; +import org.springframework.boot.actuate.health.StickyHealthIndicatorDecorator; + +/** + * Bean post processor that decorates Health Indicators with sticky UP status. + * + * @author Vladislav Fefelov + */ +public class StickyHealthIndicatorDecoratorBeanPostProcessor implements BeanPostProcessor { + + private final HealthIndicatorNameFactory nameFactory; + + private final Set healthIndicatorNamesToDecorate; + + public StickyHealthIndicatorDecoratorBeanPostProcessor(HealthIndicatorNameFactory nameFactory, + Set healthIndicatorNamesToDecorate) { + this.nameFactory = nameFactory; + this.healthIndicatorNamesToDecorate = healthIndicatorNamesToDecorate != null + ? healthIndicatorNamesToDecorate : Collections.emptySet(); + } + + public StickyHealthIndicatorDecoratorBeanPostProcessor(Set healthIndicatorNamesToDecorate) { + this(new HealthIndicatorNameFactory(), healthIndicatorNamesToDecorate); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof HealthIndicator)) { + return bean; + } + + final String healthIndicatorName = nameFactory.apply(beanName); + + if (!healthIndicatorNamesToDecorate.contains(healthIndicatorName)) { + return bean; + } + + final HealthIndicator healthIndicator = (HealthIndicator) bean; + + return new StickyHealthIndicatorDecorator(healthIndicator); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index cdfd9396c3ec..12ebc8fa31b3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -23,6 +23,7 @@ org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfig org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.health.StickyHealthIndicatorDecoratorAutoConfig,\ org.springframework.boot.actuate.autoconfigure.influx.InfluxDbHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/StickyHealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/StickyHealthEndpointAutoConfigurationTests.java new file mode 100644 index 000000000000..ec1bd3e20aa7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/StickyHealthEndpointAutoConfigurationTests.java @@ -0,0 +1,77 @@ +package org.springframework.boot.actuate.autoconfigure.health; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StickyHealthIndicatorDecoratorBeanPostProcessor}. + * + * @author Vladislav Fefelov + * @since 20.03.2019 + */ +public class StickyHealthEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(HealthIndicatorAutoConfiguration.class, + HealthEndpointAutoConfiguration.class)); + + @Test + public void healthEndpointMergeRegularAndReactive() { + this.contextRunner + .withPropertyValues("management.health.status.sticky[0]=simple") + .withUserConfiguration(HealthIndicatorConfiguration.class) + .run((context) -> { + AtomicReference indicatorState = context.getBean("simpleHealthIndicatorState", + AtomicReference.class); + HealthIndicator indicator = context.getBean("simpleHealthIndicator", + HealthIndicator.class); + + Health initialDownState = Health.down().build(); + indicatorState.set(initialDownState); + + assertThat(indicator.health()).isSameAs(initialDownState); + + Health upState = Health.up().build(); + indicatorState.set(upState); + + assertThat(indicator.health()).isSameAs(upState); + + Health nextDownState = Health.down() + .withDetail("custom", "detail") + .build(); + indicatorState.set(nextDownState); + + assertThat(indicator.health().getStatus()).isEqualTo(Status.UP); + assertThat(indicator.health().getDetails().get("originalStatus")).isEqualTo(Status.DOWN); + assertThat(indicator.health().getDetails().get("originalDetails")).isEqualTo(nextDownState.getDetails()); + }); + } + + @Configuration + static class HealthIndicatorConfiguration { + + @Bean + public AtomicReference simpleHealthIndicatorState() { + return new AtomicReference<>(Health.up().build()); + } + + @Bean + public HealthIndicator simpleHealthIndicator() { + return simpleHealthIndicatorState()::get; + } + + } + + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StickyHealthIndicatorDecorator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StickyHealthIndicatorDecorator.java new file mode 100644 index 000000000000..e3b1c00d7959 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StickyHealthIndicatorDecorator.java @@ -0,0 +1,50 @@ +package org.springframework.boot.actuate.health; + +import org.springframework.util.Assert; + +/** + * Decorator for Health Indicator that suppresses any status except UP if + * actual status was UP at least once. + * + * @author Vladislav Fefelov + */ +public class StickyHealthIndicatorDecorator implements HealthIndicator { + + private static final String ORIGINAL_STATUS_KEY = "originalStatus"; + + private static final String ORIGINAL_DETAILS_KEY = "originalDetails"; + + private final HealthIndicator delegate; + + private volatile boolean wasUp = false; + + public StickyHealthIndicatorDecorator(HealthIndicator healthIndicator) { + Assert.notNull(healthIndicator, "HealthIndicator cannot be null"); + this.delegate = healthIndicator; + } + + @Override + public Health health() { + final boolean previouslyWasUp = wasUp; + + final Health actualHealth = delegate.health(); + + if (actualHealth.getStatus() == Status.UP) { + if (!previouslyWasUp) { + wasUp = true; + } + + return actualHealth; + } + + if (previouslyWasUp) { + return Health.up() + .withDetail(ORIGINAL_STATUS_KEY, actualHealth.getStatus()) + .withDetail(ORIGINAL_DETAILS_KEY, actualHealth.getDetails()) + .build(); + } + + return actualHealth; + } + +}