burghboy555
burghboy555

Reputation: 59

Programmatically add Health Indicators to Springboot Actuator Group

Problem

I would like to programmatically include health indicators into a actuator group. They only way to do this right now is in a *.properties file.

Use Case

Depending on what beans or other properties are found in the application it would be great to automatically or manually add in Health Indicators into the readiness/liveness groups. While it seems easy enough to setting the property, in many cases developers may forget to set it up or there are old projects that people will not known what health indicators may need to be configured.

Example:

If a datasource health indicator is enabled in the application, it would be nice to autoconfigure the that health indicator into the readiness group. However we wouldn't want this if the datasource health indicator is not active.

Thanks!

Upvotes: 5

Views: 2027

Answers (3)

chauwel
chauwel

Reputation: 11

I had a similar need to instanciate and register at runtime some indicators and to add them dynamically to an actuator group.

The instanciation/registration part is performed in an implementation of ApplicationContextInitializer + SmartInitializingSingleton.

The provisioning of an actual actuator group is quite simple:

import jakarta.annotation.PostConstruct;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static com.acme.server.management.health.workers.WorkerStatusIndicatorAutoConfig.PROPERTIES_PREFIX;
import static com.acme.server.management.health.workers.WorkerStatusIndicatorInitializer.sanitize;
import static java.util.Optional.ofNullable;
import static org.springframework.util.ObjectUtils.isEmpty;

@Data
@Configuration
@ConfigurationProperties(prefix = PROPERTIES_PREFIX)
public class WorkerStatusIndicatorAutoConfig {

    static final String PROPERTIES_PREFIX = "management.endpoint.health.workers";

    String groupName = "workers";

    @Autowired
    HealthEndpointProperties healthEndpointProperties;

    final Map<String, WorkerStatusIndicatorProperties> connections = new HashMap<>();

    @PostConstruct
    void init() {
        var indicators = connections.keySet().stream().map(name -> name + "WorkerStatus").collect(Collectors.toSet())
        var group = new HealthEndpointProperties.Group();        
        group.setInclude(indicators);
        healthEndpointProperties.getGroup().put(groupName, group); 
    }
}

Upvotes: 0

devatherock
devatherock

Reputation: 4991

I think what I could come up with is what zlsmith86 has already suggested. Basically I created a custom CompositeHealthContributor bean named readinessProbe from all the HealthIndicator and HealthContributor beans that I might need, injected as Optional and included in the composite contributor if they are present. Then the value of the management.endpoint.health.group.readiness.include config can be set always to readinessProbe, the name of the custom composite health contributor bean.

Custom composite health contributor:

package io.github.devatherock.config;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReadinessProbeConfig {

    @Bean
    public CompositeHealthContributor readinessProbe(
            @Qualifier("dbHealthContributor") Optional<HealthContributor> dbHealthContributor,
            @Qualifier("mongoHealthContributor") Optional<HealthContributor> mongoHealthContributor,
            Optional<DiskSpaceHealthIndicator> diskSpaceHealthIndicator) {
        Map<String, HealthContributor> healthIndicatorMap = Arrays
                .asList(dbHealthContributor, mongoHealthContributor, diskSpaceHealthIndicator)
                .stream()
                .filter(Optional::isPresent)
                .collect(Collectors.toMap(
                        indicator -> indicator.get().getClass().getSimpleName(), Optional::get));

        return CompositeHealthContributor.fromMap(healthIndicatorMap);
    }
}

application.yml:

management:
  endpoint:
    health:
      probes:
        enabled: true
      group:
        readiness:
          include: readinessProbe 

Upvotes: 0

Hardik Uchdadiya
Hardik Uchdadiya

Reputation: 368

You need to add HealthIndicator beans to implement custom health indicators and then need to inject CustomHealthIndicator.

step 1: create custom health indicator in your application

import com.google.common.collect.ImmutableMap;
import com.netflix.appinfo.HealthCheckHandler;
import com.netflix.appinfo.InstanceInfo;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;

import java.util.Map;

/**
 * Update #getStatus() to do your detailed application check (testing for databases, external services, queues, etc.)
 * to represent your health check by indicating all its components are also functional (as opposed to a simple
 * application-is-running check).
 */
public class EurekaHealthCheckHandler implements HealthCheckHandler, ApplicationContextAware, InitializingBean {

    public final static ImmutableMap<Status, InstanceInfo.InstanceStatus> healthStatuses =
            new ImmutableMap.Builder<Status, InstanceInfo.InstanceStatus>()
                    .put(Status.UNKNOWN, InstanceInfo.InstanceStatus.UNKNOWN)
                    .put(Status.OUT_OF_SERVICE, InstanceInfo.InstanceStatus.OUT_OF_SERVICE)
                    .put(Status.DOWN, InstanceInfo.InstanceStatus.DOWN)
                    .put(Status.UP, InstanceInfo.InstanceStatus.UP)
                    .build();

    private final CompositeHealthIndicator healthIndicator;

    private ApplicationContext applicationContext;

    public EurekaHealthCheckHandler(HealthAggregator healthAggregator) {
        Assert.notNull(healthAggregator, "HealthAggregator must not be null");
        this.healthIndicator = new CompositeHealthIndicator(healthAggregator);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        final Map<String, HealthIndicator> healthIndicators = applicationContext.getBeansOfType(HealthIndicator.class);
        for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
            healthIndicator.addHealthIndicator(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus instanceStatus) {
        Status status = healthIndicator.health().getStatus();
        return healthStatuses.containsKey(status) ? healthStatuses.get(status) : InstanceInfo.InstanceStatus.UNKNOWN;
    }
}

and then implement health indicators according to your needs

import lombok.extern.slf4j.Slf4j;
import com.application.services.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;

/**
 * Spring Cloud Health Indicator (for Eureka discovery). Sends a ping to Twilio and if sent successfully reports
 * service as UP otherwise DOWN.
 */
@Component
@Slf4j
public class SmsHealthIndicator implements HealthIndicator {
    @Autowired
    private SmsService smsService;

    /**
     * Performs health check by getting first SMS from Twilio.
     */
    @Override
    public Health health() {
        try {
            smsService.testSampleMessage();
            return Health.up().build();
        } catch (Exception e) {
            log.error("HealthCheck with Twilio failed", e);
            return Health.down(e).build();
        }
    }
}

this should suffice your requirements

Upvotes: 0

Related Questions