DEV Community

Panos Sideris
Panos Sideris

Posted on

Custom Health Aggregator in Spring Boot 2

In this article, we are going to explain how to set up and customise a health endpoint in a Spring Boot application via Spring Actuator.

Table Of Contents

The code for this demo could be found here:

GitHub logo psideris89 / spring-health-aggregator-demo

Spring boot application that uses Spring Actuator and demonstrates a custom health aggregator

To upgrade gradle version to 7.1

$ ./gradlew wrapper --gradle-version=7.1 --distribution-type=bin




Getting started with Spring Boot Actuator

Spring Boot Actuator is part of the Spring ecosystem and provides a plethora of functionalities, including the ability to scan the health of your application. This is often called a health endpoint.

Health Indicator vs Health Aggregator

Health Indicator is a component that returns a health status (UP, DOWN, etc). This component could perform an action, like calling an API or querying a database or service.

Health Aggregator, as the name implies, processes the various Health Status (of the Health Indicators) and produces a general Health Status for the application.

Setting up Spring Actuator

Create a Spring Boot project

For the demo let's create a Spring Boot app with the following specs:

  • Project: Gradle
  • Language: Java 16
  • Spring Boot: 2.5.2 (or latest)
  • Packaging: jar
  • Dependencies: Spring Web, Spring Boot Actuator

Press Generate which will download a zip file and unzip the file in the directory of your preference.

Spring boot configuration

Run the application

To run the application you can either use your IDE (if you use Intellij and Java 16 make sure to update to the latest version) or you can use your terminal and the Gradle Wrapper (gradlew).

Navigate to the directory you extracted the zip and run from the terminal



$ ./gradlew bootRun


Enter fullscreen mode Exit fullscreen mode

Just by adding the Actuator in the project dependencies you enable the health auditing of your app which is accessible from /actuator/health.

As we are running the application locally we need to access http://localhost:8080/actuator/health and the status should be UP.

health status

Create a Health Indicator

You have the option to either extent the AbstractHealthIndicator or implement the HealthIndicator. In our case we choose the former.

Make sure you annotate your Health Indicator with @Component (you could use other Stereotype annotations or @Configuration if you prefer) in order for Spring to create a bean.

The following Indicator is creating a random number and conditionally returns status UP or DOWN with the error details.



package com.psideris.springhealthaggregatordemo;

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

import java.util.Random;

@Component
public class NumberAppHealthIndicator extends AbstractHealthIndicator {

    @Override
    protected void doHealthCheck(Health.Builder builder) {
        int number = getRandomNumber();

        if (number > 5) {
            builder.up().build();
        } else {
            builder.down().withDetail("error", "Number was less than 5").build();
        }
    }

    private int getRandomNumber() {
        return new Random().ints(1, 10).findFirst().orElseThrow(() -> new RuntimeException("Failed to generate random number"));
    }
}



Enter fullscreen mode Exit fullscreen mode

Bear in mind that you can create as many Health Indicators as you want but Spring checks the health of natively supported services (e.g. SQL DBs, mongoDB, elasticsearch, etc) without requiring any customisation, just adding the Actuator dependency.

Create a Health Aggregator

For the heath aggregator we are going to implement the
StatusAggregator.

As we mentioned before the aggregator accumulates the health status from multiple Indicators, which means you can define the logic to indicate when the app is up or down.



package com.psideris.springhealthaggregatordemo;

import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class AppHealthAggregator implements StatusAggregator {

    @Override
    public Status getAggregateStatus(Set<Status> statuses) {
        boolean statusUp = statuses.stream().allMatch(s -> s.equals(Status.UP));

        return statusUp ? Status.UP : Status.DOWN;
    }
}



Enter fullscreen mode Exit fullscreen mode

Advanced configuration

Show details in the health status

To display additional details you can add in the application.properties or application.yml file.



// application.yml

management:
  endpoint:
    health:
      show-details: always


Enter fullscreen mode Exit fullscreen mode


// application.properties

management.endpoint.health.show-details=always


Enter fullscreen mode Exit fullscreen mode

The health endpoint should now include the following details.

health status with details

Configure the health URL

To configure the url which exposes the health status you can add the following properties in the application.properties or application.yml file.



// application.yml

management:
  endpoints:
    web:
      base-path: /api
      path-mapping:
        health: health-status


Enter fullscreen mode Exit fullscreen mode


// application.properties

management.endpoints.web.base-path=/api
management.endpoints.web.path-mapping.health=health-status



Enter fullscreen mode Exit fullscreen mode

Now the url that we need to access is /api/health-status.

Accessing the Status of individual Health Indicator

By default when creating a Health Indicator it is being exposed in the URL that matches its name (Spring removes the HealthIndicator suffix from the url).

If you provide a value inside the annotation then the URL changes to reflect that. In our case the new one is http://localhost:8080/actuator/health/numberService.



@Component("numberService")
public class NumberAppHealthIndicator extends AbstractHealthIndicator {

<span class="o">...</span>
Enter fullscreen mode Exit fullscreen mode

}

Enter fullscreen mode Exit fullscreen mode




Additional Resources

You can find more about Spring Boot Actuator in the official documentation

https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html

Top comments (3)

Collapse
 
bsl profile image
BELAGGOUN

Hello , thank you for this tutorial.
I have created a service that implements HealthIndicator Interface , and based on the health components (HealthIndicators Beans) status I perform some business logic.
ex: if diskSpaceHealthIndicator.getStatus()=="UP" I perform Action1 else Action2.
I'm having hard time on how to mock the HealthIndicator beans , I'm using @MockBean PingHealthContributor pingHealthContributor;
but when I do :
when(when(((HealthIndicator)
(pingHealthContributor)).health().getStatus()).thenReturn(health.getStatus());
the getStatus is returning null pointer exception.
Please let me know if you have any Idea on how to mock the actuator health beans .
Thank you .

Collapse
 
psideris89 profile image
Panos Sideris

What you could do is to override the Health of the dependencies in your tests.

public class PublicApiIndicatorTest {

    // PublicApiIndicator requires BookHealthIndicator bean (Autowiring by constructor)
    private PublicApiIndicator publicApiIndicator;
    private BookHealthIndicator bookHealthIndicator;

    @Test
    public void test_up() {
        bookHealthIndicator = new BookHealthIndicator() {
            @Override
            public Health health() {
                return Health.up().build();
            }
        };
        publicApiIndicator = new PublicApiIndicator(bookHealthIndicator);

        Health health = publicApiIndicator.health();
        Assertions.assertEquals("UP", health.getStatus().getCode());
    }

    @Test
    public void test_down() {
        bookHealthIndicator = new BookHealthIndicator() {
            @Override
            public Health health() {
                return Health.down().build();
            }
        };
        publicApiIndicator = new PublicApiIndicator(bookHealthIndicator);

        Health health = publicApiIndicator.health();
        Assertions.assertEquals("DOWN", health.getStatus().getCode());
    }
}
Enter fullscreen mode Exit fullscreen mode

I hope that works!

Collapse
 
bsl profile image
BELAGGOUN

Thank you so much, for your answer, I have implemented a test using your proposal and its working. In my Implementation I'm autowiring a HealthContributor It wasn't possible for me to create an instance of each component because the health method is final(error message when overriding the healt() method) , so I ended by creating a HealthIndicator object that I passed in place of the HealthContributor[] to the service . It was intercepted by my service and the health status was the exactly the one that I defined in the override (test).