Skip to content

Introduce Sticky health indicator #16275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -42,6 +44,11 @@ public class HealthIndicatorProperties {
*/
private final Map<String, Integer> httpMapping = new HashMap<>();

/**
* Health indicators that must always stay UP in case they were UP at least once.
*/
private Set<String> sticky = new HashSet<>();

public List<String> getOrder() {
return this.order;
}
Expand All @@ -56,4 +63,12 @@ public Map<String, Integer> getHttpMapping() {
return this.httpMapping;
}

public Set<String> getSticky() {
return sticky;
}

public void setSticky(Set<String> sticky) {
this.sticky = sticky;
}

}
Original file line number Diff line number Diff line change
@@ -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<String> healthIndicatorNamesToDecorate;

public StickyHealthIndicatorDecoratorBeanPostProcessor(HealthIndicatorNameFactory nameFactory,
Set<String> healthIndicatorNamesToDecorate) {
this.nameFactory = nameFactory;
this.healthIndicatorNamesToDecorate = healthIndicatorNamesToDecorate != null
? healthIndicatorNamesToDecorate : Collections.emptySet();
}

public StickyHealthIndicatorDecoratorBeanPostProcessor(Set<String> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,\
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Health> 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<Health> simpleHealthIndicatorState() {
return new AtomicReference<>(Health.up().build());
}

@Bean
public HealthIndicator simpleHealthIndicator() {
return simpleHealthIndicatorState()::get;
}

}


}
Original file line number Diff line number Diff line change
@@ -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;
}

}