Health Checks and Metrics in Spring

Spring Boot provides a comprehensive metrics package which you can use and extend with a minimum of fuss, but on occasion I've wished to add the same functionality to a Spring web application which doesn't use Spring Boot. 

We can do this using Dropwizard's metrics package without too much difficulty but it does need a little bit of glue. I won't go into detail about how to write healthchecks and metrics but I will elaborate on the wiring. It is my aim here to let Spring manage any healthcheck and metrics beans, and let the Metrics package handle the web endpoints.

Firstly you'll need a couple of dependencies for the metrics package. Here are the gradle entries I used:

compile 'com.codahale.metrics:metrics-core:3.0.2'
compile 'com.codahale.metrics:metrics-servlets:3.0.2'

Let's say I want a health check for the database connection. Here's a simple implementation which checks that a connection can be obtained:

@Component
public class DatabaseConnectionHealthCheck extends HealthCheck {

    public static final String HEALTHCHECK_NAME = "databaseConnection";

    @Autowired
    private HealthCheckRegistry healthCheckRegistry;

    @Autowired
    private DataSource dataSource;

    @PostConstruct
    public void addToRegistry() {
        healthCheckRegistry.register(HEALTHCHECK_NAME, this);
    }

    @Override
    protected Result check() {

      try (Connection connection = dataSource.getConnection()) {
          return HealthCheck.Result.healthy();
      } catch (SQLException e) {
          return HealthCheck.Result.unhealthy(e.getMessage());
      }
    }
}

This class is wired into our app as a Spring bean, and importantly it has a @PostConstruct method which wires it into the Metrics' own context, the HealthCheckRegistry. Now the fiddly part is injecting this registry, and the MetricsRegistry, into the Spring context as well.

We will need a few classes of our own to do this. The Metrics endpoints may be configured in our web.xml, eg:

<servlet>
    <servlet-name>metrics</servlet-name>
    <servlet-class>com.codahale.metrics.servlets.AdminServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>metrics</servlet-name>
    <url-pattern>/metrics/*</url-pattern>
</servlet-mapping>

The AdminServlet looks for instances of MetricsRegistry and HealthCheckRegistry to find instances of metrics and healthchecks to handle. We can supply our own Spring configuration which creates these:

@Configuration
public class MetricsConfiguration {

    @Bean
    public HealthCheckRegistry newHealthCheckRegistry() {
        return new HealthCheckRegistry();
    }

    @Bean
    public MetricRegistry newMetricRegistry() {
        return new MetricRegistry();
    }
}

This creates the two registries as Spring beans. In order to make these visible to the Metrics code (i.e. the AdminServlet) we can use a Spring ServletContextListener: 

public class MetricsServletsWiringContextListener implements ServletContextListener {

  @Autowired
  private MetricRegistry metricRegistry;

  @Autowired
  private HealthCheckRegistry healthCheckRegistry;

  private MetricsServletContextListener metricsServletContextListener;
  private HealthCheckServletContextListener healthCheckServletContextListener;

  @Override
  public void contextInitialized(ServletContextEvent event) {
          WebApplicationContextUtils.getRequiredWebApplicationContext(event.getServletContext())
              .getAutowireCapableBeanFactory()
              .autowireBean(this);

      metricsServletContextListener = new MetricsServletContextListener(metricRegistry);
      healthCheckServletContextListener = new HealthCheckServletContextListener(healthCheckRegistry);

      metricsServletContextListener.contextInitialized(event);
      healthCheckServletContextListener.contextInitialized(event);
  }

  @Override
  public void contextDestroyed(ServletContextEvent event) {
  }

}

Let's unpack this a bit. Firstly the two registries are @Autowired in from Spring. Next we have two classes, MetricsServletContextListener and HealthcheckServletContextListener. More on these later.

The meat of this class lies in the contextInitialized method. This is called when the web context has been initialized. The first thing the method does is autowire this listener itself into the Spring context:

WebApplicationContextUtils.getRequiredWebApplicationContext(event.getServletContext())
            .getAutowireCapableBeanFactory()
            .autowireBean(this);

Then we take the two aforementioned listener classes and inject the registries into them. The magic happens inside their contextInitialized methods. This part is a bit of a hack - what we're doing here is re-using some Metrics servlet context code to make various beans available as servlet context attributes.

Now these two other classes are subclasses of ContextListeners supplied by the Metrics package. Here is the health check one:

public class HealthCheckServletContextListener extends  HealthCheckServlet.ContextListener {

private final HealthCheckRegistry registry;

public HealthCheckServletContextListener(HealthCheckRegistry metricRegistry) {
    this.registry = metricRegistry;
}

@Override
protected HealthCheckRegistry getHealthCheckRegistry() {
    return registry;
}

}

If one were to take a look inside the contextInitialized method one would see things like this:

context.setAttribute(METRICS_REGISTRY, getMetricRegistry());

Now one could perform these steps manually without extending the Metrics ContextListener classes, but I'll leave that as an exercise for the reader. The MetricsServletContextListener works the same way, but extends MetricsServlet.ContextListener instead.

Finally one needs to configure the MetricsServletsWiringContextListener in the web.xml:

<listener>
      <listener-class>uk.co.tpplc.digitaltrade.app.metrics.MetricsServletsWiringContextListener</listener-class>
  </listener>

And there you have it. It's a bit convoluted but at this point one can add HealthChecks and Metrics as required.