Java: adding custom health indicator to Spring Boot Actuator

If you have enabled Actuator in your Spring Boot application, you can add custom status metrics to the standard health check at ‘/actuator/health’.

Additionally, your custom health indicator can signal an UP/DOWN status that propagates to the main level status and can then be used by an external monitoring/alerting solutions or as an indicator to container orchestration (e.g. Kubernetes health check).

Spring Boot prerequisites

If you are reading this article, I’m assuming you have enabled the Actuator package already, and that you have already proven that you can can reach endpoints such as /actuator and /actuator/health for your Spring Boot web application.

If not, go through this article from Baeldung for the basic setup.

You should be sure that you have included the package in build.gradle

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-actuator'
   ...
}

Or pom.xml if using Maven.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
  <scope>compile</scope>
</dependency>

And expose the management endpoints in application.properties.  Set ‘health.show-details’ so the custom health is displayed at ‘/actuator/health’.

management.endpoints.enabled-by-default=true
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details: always

Simple HealthIndicator

In its most basic form, define a @Component custom class that implements HealthIndicator to insert a custom health check.

...
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class SimpleHealthIndicator implements HealthIndicator {

  // always reports UP
  @Override
  public Health health() {
    Map<String,String> map = new HashMap<String,String>();
    map.put("foo","bar");
    map.put("my","value");
    return new Health.Builder().up().withDetail("simple", map).build();
    //return new Health.Builder().down().withDetail("simple", map).build();
  }

}

When pulling up the ‘/actuator/health’ endpoint with a browser, you should get a response similar to below.

{
  "status": "UP",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 502467059712,
        "free": 385737367552,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "simple": {
      "status": "UP",
      "details": {
        "simple": {
          "foo": "bar",
          "my": "value"
        }
      }
    }
  }
}

You can see our “simple” status with the key/value pairs we defined, and status=UP.  Modifying the Health.Builder to instead use down() would set our child status=DOWN and that would propagate to the main status=DOWN as well.

Custom HealthIndicator looking at application specific objects

A health check typically needs to look at other objects in the Spring application context to determine health.  You might need to check a Controller, or CrudRepository, or Service object.  Luckily Spring @Autowired injection provides easy access to other application objects.

For example, if our custom health check autowires a ProductRepository that can go to the database and look at our Product inventory, we can find objects with a low inventory count and report back poor health (status=DOWN) if we are out of stock.

@Component
public class ProductHealthIndicator implements HealthIndicator {

@Autowired
protected ProductRepository productRepository;

@Override
public Health health() {
  int lowestCount = Integer.MAX_VALUE;

  // holds list of Product with low inventory count
  Map<String,Integer> productCounts = new HashMap<String,Integer>();

  for (Product p:productRepository.findProductWithLowInventoryCount()) {
    productCounts.put(p.getName(),Integer.valueOf(p.getCount()));

    // find lowest inventory count
    if(p.getCount()<lowestCount)
      lowestCount = p.getCount(); 
  }

  // if any of the products are out of stock, report health down
  if (lowestCount>0) 
    return new Health.Builder().up().withDetail("products", productCounts).build();
  else
    return new Health.Builder().down().withDetail("products", productCounts).build();
  }

}

...

This will produce an ‘/actuator/health’ status showing our “product” status=UP, and reporting that we have low inventory on Coffee Cups (<=3).

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "H2",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 502467059712,
        "free": 385729576960,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "product": {
      "status": "UP",
      "details": {
        "products": {
          "Coffee Cup": 3
        }
      }
    }
  }
}

However, it will not show status=DOWN until the number of Coffee Cups is down to 0 (out of stock).  When our custom health check status=DOWN, this propagates to the main level and would look like below.

Also, instead of an HTTP status code of 200, the response becomes a 503 which signals error to monitoring/orchestration systems.

{
  "status": "DOWN",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "H2",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 502467059712,
        "free": 385729576960,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "product": {
      "status": "DOWN",
      "details": {
        "products": {
          "Coffee Cup": 3
        }
      }
    }
  }
}

Here is the full code to ProductHealthIndicator.java

 

REFERENCES

javadevjournal.com, custom health endpoint and controller endpoint