Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

Spring Boot provides a few different ways to inspect the status and health of a running application and its components. Among those approaches, the HealthContributor and HealthIndicator APIs are two of the notable ones.

In this tutorial, we're going to get familiar with these APIs, learn how they work, and see how we can contribute custom information to them.

2. Dependencies

Health information contributors are part of the Spring Boot actuator module, so we need the appropriate Maven dependency:

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

3. Built-in HealthIndicators

Out of the box, Spring Boot registers many HealthIndicators to report the healthiness of a particular application aspect.

Some of those indicators are almost always registered, such as DiskSpaceHealthIndicator or PingHealthIndicator. The former reports the current state of the disk and the latter serves as a ping endpoint for the application.

On the other hand, Spring Boot registers some indicators conditionally. That is if some dependencies are on the classpath or some other conditions are met, Spring Boot might register a few other HealthIndicators, too. For instance, if we're using relational databases, then Spring Boot registers DataSourceHealthIndicator. Similarly, it'll register CassandraHealthIndicator if we happen to use Cassandra as our data store.

In order to inspect the health status of a Spring Boot application, we can call the /actuator/health endpoint. This endpoint will report an aggregated result of all registered HealthIndicators.

Also, to see the health report from one specific indicator, we can call the /actuator/health/{name} endpoint. For instance, calling the /actuator/health/diskSpace endpoint will return a status report from the DiskSpaceHealthIndicator:

{
  "status": "UP",
  "details": {
    "total": 499963170816,
    "free": 134414831616,
    "threshold": 10485760,
    "exists": true
  }
}

4. Custom HealthIndicators

In addition to the built-in ones, we can register custom HealthIndicators to report the health of a component or subsystem. In order to that, all we have to do is to register an implementation of the HealthIndicator interface as a Spring bean.

For instance, the following implementation reports a failure randomly:

@Component
public class RandomHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        double chance = ThreadLocalRandom.current().nextDouble();
        Health.Builder status = Health.up();
        if (chance > 0.9) {
            status = Health.down();
        }
        return status.build();
    }
}

According to the health report from this indicator, the application should be up only 90% of the time. Here we're using Health builders to report the health information.

In reactive applications, however, we should register a bean of type ReactiveHealthIndicator. The reactive health() method returns a Mono<Health> instead of a simple Health. Other than that, other details are the same for both web application types.

4.1. Indicator Name

To see the report for this particular indicator, we can call the /actuator/health/random endpoint. For instance, here's what the API response might look like:

{"status": "UP"}

The random in the /actuator/health/random URL is the identifier for this indicator. The identifier for a particular HealthIndicator implementation is equal to the bean name without the HealthIndicator suffix. Since the bean name is randomHealthIdenticator, the random prefix will be the identifier.

With this algorithm, if we change the bean name to, say, rand:

@Component("rand")
public class RandomHealthIndicator implements HealthIndicator {
    // omitted
}

Then the indicator identifier will be rand instead of random.

4.2. Disabling the Indicator

To disable a particular indicator, we can set the management.health.<indicator_identifier>.enabled” configuration property to false. For instance, if we add the following to our application.properties:

management.health.random.enabled=false

Then Spring Boot will disable the RandomHealthIndicator. To activate this configuration property, we should also add the @ConditionalOnEnabledHealthIndicator annotation on the indicator:

@Component
@ConditionalOnEnabledHealthIndicator("random")
public class RandomHealthIndicator implements HealthIndicator { 
    // omitted
}

Now if we call the /actuator/health/random, Spring Boot will return a 404 Not Found HTTP response:

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = "management.health.random.enabled=false")
class DisabledRandomHealthIndicatorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void givenADisabledIndicator_whenSendingRequest_thenReturns404() throws Exception {
        mockMvc.perform(get("/actuator/health/random"))
          .andExpect(status().isNotFound());
    }
}

Please note that disabling built-in or custom indicators is similar to each other. Therefore, we can apply the same configuration to the built-in indicators, too.

4.3. Additional Details

In addition to reporting the status, we can attach additional key-value details using the withDetail(key, value):

public Health health() {
    double chance = ThreadLocalRandom.current().nextDouble();
    Health.Builder status = Health.up();
    if (chance > 0.9) {
        status = Health.down();
    }

    return status
      .withDetail("chance", chance)
      .withDetail("strategy", "thread-local")
      .build();
}

Here we're adding two pieces of information to the status report. Also, we can achieve the same thing by passing a Map<String, Object> to the withDetails(map) method:

Map<String, Object> details = new HashMap<>();
details.put("chance", chance);
details.put("strategy", "thread-local");
        
return status.withDetails(details).build();

Now if we call the /actuator/health/random, we might see something like:

{
  "status": "DOWN",
  "details": {
    "chance": 0.9883560157173152,
    "strategy": "thread-local"
  }
}

We can verify this behavior with an automated test, too:

mockMvc.perform(get("/actuator/health/random"))
  .andExpect(jsonPath("$.status").exists())
  .andExpect(jsonPath("$.details.strategy").value("thread-local"))
  .andExpect(jsonPath("$.details.chance").exists());

Sometimes an exception occurs while communicating to a system component such as Database or Disk. We can report such exceptions using the withException(ex) method:

if (chance > 0.9) {
    status.withException(new RuntimeException("Bad luck"));
}

We can also pass the exception to the down(ex) method we saw earlier:

if (chance > 0.9) {
    status = Health.down(new RuntimeException("Bad Luck"));
}

Now the health report will contain the stack trace:

{
  "status": "DOWN",
  "details": {
    "error": "java.lang.RuntimeException: Bad Luck",
    "chance": 0.9603739107139401,
    "strategy": "thread-local"
  }
}

4.4. Details Exposure

The management.endpoint.health.show-details configuration property controls the level of details each health endpoint can expose. 

For instance, if we set this property to always, then Spring Boot will always return the details field in the health report, just like the above example.

On the other hand, if we set this property to never, then Spring Boot will always omit the details from the output. There is also the when_authorized value which exposes the additional details only for authorized users. A user is authorized if and only if:

  • She's authenticated
  • And she possesses the roles specified in the management.endpoint.health.roles configuration property

4.5. Health Status

By default, Spring Boot defines four different values as the health Status:

  • UP — The component or subsystem is working as expected
  • DOWN — The component is not working
  • OUT_OF_SERVICE — The component is out of service temporarily
  • UNKNOWN — The component state is unknown

These states are declared as public static final instances instead of Java enums. So it's possible to define our own custom health states. To do that, we can use the status(name) method:

Health.Builder warning = Health.status("WARNING");

The health status affects the HTTP status code of the health endpoint. By default, Spring Boot maps the DOWN, and OUT_OF_SERVICE states to throw a 503 status code. On the other hand, UP and any other unmapped statuses will be translated to a 200 OK status code.

To customize this mapping, we can set the management.endpoint.health.status.http-mapping.<status> configuration property to the desired HTTP status code number:

management.endpoint.health.status.http-mapping.down=500
management.endpoint.health.status.http-mapping.out_of_service=503
management.endpoint.health.status.http-mapping.warning=500

Now Spring Boot will map the DOWN status to 500, OUT_OF_SERVICE to 503, and WARNING to 500 HTTP status codes:

mockMvc.perform(get("/actuator/health/warning"))
  .andExpect(jsonPath("$.status").value("WARNING"))
  .andExpect(status().isInternalServerError());

Similarly, we can register a bean of type HttpCodeStatusMapper to customize the HTTP status code mapping:

@Component
public class CustomStatusCodeMapper implements HttpCodeStatusMapper {

    @Override
    public int getStatusCode(Status status) {
        if (status == Status.DOWN) {
            return 500;
        }
        
        if (status == Status.OUT_OF_SERVICE) {
            return 503;
        }
        
        if (status == Status.UNKNOWN) {
            return 500;
        }

        return 200;
    }
}

The getStatusCode(status) method takes the health status as the input and returns the HTTP status code as the output. Also, it's possible to map custom Status instances:

if (status.getCode().equals("WARNING")) {
    return 500;
}

By default, Spring Boot registers a simple implementation of this interface with default mappings. The SimpleHttpCodeStatusMapper is also capable of reading the mappings from the configuration files, as we saw earlier.

5. Health Information vs Metrics

Non-trivial applications usually contain a few different components. For instance, consider a Spring Boot applications using Cassandra as its database, Apache Kafka as its pub-sub platform, and Hazelcast as its in-memory data grid.

We should use HealthIndicators to see whether the application can communicate with these components or not. If the communication link fails or the component itself is down or slow, then we have an unhealthy component that we should be aware of. In other words, these indicators should be used to report the healthiness of different components or subsystems.

On the contrary, we should avoid using HealthIndicators to measure values, count events, or measure durations. That's why we have metrics. Put simply, metrics are a better tool to report CPU usage, load average, heap size, HTTP response distributions, and so on.

6. Conclusion

In this tutorial, we saw how to contribute more health information to actuator health endpoints. Moreover, we had in-depth coverage of different components in the health APIs such as HealthStatus, and the status of HTTP status mapping.

To wrap things up, we had a quick discussion on the difference between health information and metrics and also, learned when to use each of them.

As usual, all the examples are available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Christopher Barham
Christopher Barham
27 days ago

Nice article! instead of implementing a HealthIndicator, could (usually) extend a base implementation AbstractHealthIndicator – https://docs.spring.io/autorepo/docs/spring-boot/current/api/org/springframework/boot/actuate/health/AbstractHealthIndicator.html

Loredana Crusoveanu
12 days ago

Hi Christopher,
Thanks, that’s a good option as well. However, implementing the interface gives us more room for customization.

Comments are closed on this article!