11 November 2014

Another great feature provided by Spring Boot is the ability to expose Spring Boot Custom Health Information about your application with very little effort. The simple approach is to implement the HealthIndicator interface and register your custom indicator as a Spring bean (or have it be component scanned). This is great for one-off indicators or in small projects. But what happens when you want to have a common set of health indicators that you can use in all of your services (after all, one of the promises of Spring Boot is that it makes it trivial to spin up new "micro" services)? What do you do if not all of your services have the same components that need to be part of your suite of health indicators? The solution is to leverage the auto configuration infrastructure provided by Spring Boot. The first step is to include the proper dependencies in your health indicator shared library:

ext {
    springBootVersion = '1.1.8.RELEASE'
}

dependencies {
    compile "org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}"
    compile "org.springframework.boot:spring-boot-actuator:${springBootVersion}"
}

The spring-boot-autoconfigure dependency provides the annotations that we will use to mark our configuration as able to partake in auto configuration. The spring-boot-actuator dependency provides the HealthIndicator interface and other related classes needed to implement the health indicators. The next step is to create a custom health indicator implementation:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

import kafka.consumer.ConsumerConfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;

public class KafkaConsumerHealthIndicator implements HealthIndicator {

    private static final Logger log = LoggerFactory.getLogger(KafkaConsumerHealthIndicator.class);

    private final static Long TIMEOUT = TimeUnit.SECONDS.toMillis(3);

    private final String host;

    private final int port;

    public KafkaConsumerHealthIndicator(final ConsumerConfig consumerConfig) {
        this.host = consumerConfig.zkConnect().split(":")[0];
        this.port = Integer.valueOf(consumerConfig.zkConnect().split(":")[1]);
    }

    @Override
    public Health health() {
        Socket socket = null;

        try {
            socket = new Socket();
            socket.connect(new InetSocketAddress(host, port), TIMEOUT.intValue());
            return Health.up().build();
        } catch (final Exception e) {
            return Health.down(e).build();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (final IOException e) {
                    log.debug("Unable to close Kafka consumer socket.", e);
                }
            }
        }
    }
}

In this example, the health indicator attempts to make a socket connection to the connection string provided by the Kafka ConsumerConfig. It’s a simple test to see if ZooKeeper is listening at the configured host/port. The next step is to create an automatic configuration that will define and register this health indicator if the presence of a bean of type ConsumerConfig is detected:

import java.util.Map;

import kafka.consumer.ConsumerConfig;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration;
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.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@AutoConfigureBefore({ EndpointAutoConfiguration.class })
@AutoConfigureAfter({ HealthIndicatorAutoConfiguration.class })
public class CustomHealthIndicatorAutoConfiguration {

    /**
     * Configuration for Kafka consumer health indicators.
     *
     * @author jpearlin
     * @since 1.0.0
     */
    @Configuration
    @ConditionalOnBean(ConsumerConfig.class)
    @ConditionalOnExpression("${health.kafka.consumer.enabled:true}")
    public static class KafkaConsumerHealthIndicatorConfiguration {

        @Autowired
        private HealthAggregator healthAggregator;

        @Autowired(required = false)
        private Map<String, ConsumerConfig> consumerConfigs;

        @Bean
        @ConditionalOnMissingBean(name = "kafkaConsumerHealthIndicator")
        public HealthIndicator kafkaConsumerHealthIndicator() {
            if (this.consumerConfigs.size() == 1) {
                return new KafkaConsumerHealthIndicator(this.consumerConfigs.values().iterator().next());
            }

            final CompositeHealthIndicator composite = new CompositeHealthIndicator(this.healthAggregator);
            for (final Map.Entry<String, ConsumerConfig> entry : this.consumerConfigs.entrySet()) {
                composite.addHealthIndicator(entry.getKey(), new KafkaConsumerHealthIndicator(entry.getValue()));
            }
            return composite;
        }
    }
}

This auto configuration ensures that it is enabled after the default HealthIndicatorAutoConfiguration provided by the spring-boot-actuator dependency has been loaded. It also defines one Spring Configuration that is conditionally loaded based on the presence of a bean of type ConsumerConfig. It can also be manually disabled by setting the health.kafka.consumer.enabled property to false. The configuration also ensures that if more than one bean of type ConsumerConfig is present, a CompositeHealthIndicator is created. The final piece required to tie all of this together is to provide Spring with the required metadata file that instructs it on which classes represent auto-configuration. To do this, create a file named spring.factories in the src/main/resources/META-INF directory of your shared health indicator library:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.health.autoconfigure.CustomHealthIndicatorAutoConfiguration

Spring Boot will automatically scan for these metadata files when your application starts and register your custom auto configuration class to be enacted if it detects the EnableAutoConfiguration annotation on any of your Java-based configuration classes. Now you can provide a full suite of health indicators that will be enabled if and only if certain conditions are met, as defined by the various configurations provided by your auto-configuration class!

comments powered by Disqus