Custom Actuator Health Indicator In Spring Boot

As a senior Java Spring Boot developer, I'm excited to share a deep dive into customizing Actuator health indicators – a powerful feature for monitoring your application's well-being.


Beyond the Basics: Crafting Custom Actuator Health Indicators in Spring Boot

As Spring Boot developers, we all rely heavily on Actuator for production-ready monitoring. Its /actuator/health endpoint provides a quick snapshot of our application's status, encompassing everything from database connectivity to disk space. But what happens when you need to monitor something specific to your application's domain – a custom external service, an internal cache's state, or the health of a critical business process?

This is where custom Actuator health indicators come into play. They empower you to extend Spring Boot's health reporting capabilities, providing a more comprehensive and tailored view of your application's operational health.

In this blog post, we'll explore how to create custom Actuator health indicators, complete with practical examples to guide you.

Why Custom Health Indicators?

While Spring Boot's out-of-the-box health indicators cover many common scenarios (database, disk space, Kafka, Redis, etc.), they can't possibly anticipate every unique dependency or internal state that your application might rely on. Custom health indicators allow you to:

  • Monitor Specific External Services: Check the reachability or responsiveness of a third-party API crucial to your application.
  • Track Internal Component Health: Report on the status of a custom cache, a message queue consumer, or a specialized processing engine.
  • Validate Business Logic Dependencies: Ensure that a particular configuration or resource vital for a business process is correctly loaded and operational.
  • Provide Granular Insights: Offer more detailed "UP" or "DOWN" status with additional information relevant to your domain.

The HealthIndicator Interface

The cornerstone of custom health indicators is the org.springframework.boot.actuate.health.HealthIndicator interface. This simple functional interface requires you to implement a single method:

public interface HealthIndicator {
    Health health();
}

The health() method returns a org.springframework.boot.actuate.health.Health object, which represents the current health status. The Health class provides methods to build this status, typically using Health.up(), Health.down(), or Health.unknown(), along with withDetail(key, value) to add additional information.

Step-by-Step: Creating a Custom Health Indicator

Let's walk through an example of creating a custom health indicator that checks the connectivity to an imaginary "Licensing Service."

1. Define Your Custom Health Indicator

Create a new Java class that implements HealthIndicator and annotate it with @Component to make it a Spring bean.

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

@Component
public class LicensingServiceHealthIndicator implements HealthIndicator {

    private final LicensingService licensingService; // Assume this is a service to check

    public LicensingServiceHealthIndicator(LicensingService licensingService) {
        this.licensingService = licensingService;
    }

    @Override
    public Health health() {
        try {
            // Simulate checking the licensing service connectivity
            if (licensingService.isServiceReachable()) {
                return Health.up()
                             .withDetail("message", "Licensing Service is reachable and operational.")
                             .build();
            } else {
                return Health.down()
                             .withDetail("message", "Licensing Service is currently unreachable.")
                             .build();
            }
        } catch (Exception e) {
            return Health.down(e)
                         .withDetail("message", "Error checking Licensing Service connectivity.")
                         .build();
        }
    }
}

2. (Optional) Create a Dummy Service for Demonstration

For the sake of this example, let's create a simple LicensingService interface and its implementation:

// LicensingService.java
public interface LicensingService {
    boolean isServiceReachable();
}

// LicensingServiceImpl.java
import org.springframework.stereotype.Service;

@Service
public class LicensingServiceImpl implements LicensingService {

    private boolean serviceStatus = true; // Simulate initial UP state

    @Override
    public boolean isServiceReachable() {
        // In a real application, this would involve network calls, API checks, etc.
        // For demonstration, we'll toggle its status.
        return serviceStatus;
    }

    // Method to simulate service going down/up (for testing)
    public void setServiceStatus(boolean status) {
        this.serviceStatus = status;
    }
}

3. Enable Actuator and Test

Make sure you have Actuator dependencies in your pom.xml (or build.gradle):

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

Now, run your Spring Boot application. Navigate to /actuator/health (or /actuator/health/licensingService if you want to see only this indicator's status). You should see an entry for licensingService with its status:

{
    "status": "UP",
    "components": {
        "diskSpace": {
            "status": "UP",
            "details": {
                "total": 249774643200,
                "free": 149774643200,
                "threshold": 10485760
            }
        },
        "licensingService": {
            "status": "UP",
            "details": {
                "message": "Licensing Service is reachable and operational."
            }
        },
        // ... other default indicators
    }
}

If you were to simulate the LicensingService going down (e.g., by calling licensingService.setServiceStatus(false) in a test endpoint), the health status would reflect that:

{
    "status": "DOWN", // Overall status could be DOWN if this is critical
    "components": {
        // ...
        "licensingService": {
            "status": "DOWN",
            "details": {
                "message": "Licensing Service is currently unreachable."
            }
        },
        // ...
    }
}

More Advanced Scenarios

1. Grouping Health Indicators

For complex applications, you might have multiple custom indicators related to a specific subsystem. You can group them together by implementing CompositeHealthContributor or NamedHealthContributor. However, for most use cases, simply having individual HealthIndicator beans is sufficient as Actuator automatically aggregates them.

2. Asynchronous Health Checks

If your health checks involve time-consuming operations (e.g., calling a slow external API), you might want to perform them asynchronously to avoid blocking the /actuator/health endpoint. You can achieve this by using Spring's @Async annotation on a helper method or by scheduling the health check to update a cached status.

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.PostConstruct;
import org.springframework.scheduling.annotation.Scheduled;

@Component
public class ExpensiveOperationHealthIndicator implements HealthIndicator {

    private final AtomicReference<Health> currentHealth = new AtomicReference<>(Health.unknown().build());

    @PostConstruct
    public void init() {
        performHealthCheck(); // Initial check on startup
    }

    @Scheduled(fixedRate = 60000) // Run every 60 seconds
    public void performHealthCheck() {
        try {
            // Simulate a long-running, expensive check
            Thread.sleep(5000); // 5 seconds
            boolean success = Math.random() > 0.2; // 80% chance of being UP

            if (success) {
                currentHealth.set(Health.up().withDetail("lastChecked", System.currentTimeMillis()).build());
            } else {
                currentHealth.set(Health.down().withDetail("reason", "Simulated failure").build());
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            currentHealth.set(Health.down(e).withDetail("reason", "Interrupted during check").build());
        } catch (Exception e) {
            currentHealth.set(Health.down(e).withDetail("reason", "Unexpected error during check").build());
        }
    }

    @Override
    public Health health() {
        return currentHealth.get();
    }
}

Important Note: When using @Scheduled, ensure you have @EnableScheduling in your main application class or a configuration class.

3. Conditional Health Indicators

You might want to enable a custom health indicator only if a certain property is set or a specific bean is present. You can use Spring's @ConditionalOnProperty or @ConditionalOnBean annotations for this.

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

@Component
@ConditionalOnProperty(name = "app.feature.experimental.enabled", havingValue = "true")
public class ExperimentalFeatureHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        // Logic to check the health of the experimental feature
        boolean featureWorking = true; // Replace with actual logic
        if (featureWorking) {
            return Health.up().withDetail("message", "Experimental feature is active and healthy.").build();
        } else {
            return Health.down().withDetail("message", "Experimental feature is experiencing issues.").build();
        }
    }
}

This indicator will only be registered if app.feature.experimental.enabled=true is present in your application.properties.

Best Practices for Custom Health Indicators

  • Keep them Lightweight: Health checks should be fast and non-blocking. Avoid heavy computations or long-running operations directly within the health() method. If necessary, use asynchronous patterns as shown above.
  • Provide Meaningful Details: Use withDetail(key, value) to provide context and diagnostic information. This is invaluable when debugging issues.
  • Handle Exceptions Gracefully: Always wrap your health check logic in a try-catch block and return Health.down(exception) if an error occurs.
  • Consider Impact on Overall Health: If a custom indicator is critical, its DOWN status will usually propagate to the overall application status. If it's merely informative, you might consider custom status mappings or grouping for specific dashboards.
  • Document Your Indicators: Clearly document what each custom health indicator monitors and what its various states signify.
  • Test Thoroughly: Test your custom health indicators under various conditions (UP, DOWN, errors) to ensure they accurately reflect the system's state.

Conclusion

Custom Actuator health indicators are a powerful tool in your Spring Boot monitoring arsenal. By extending the built-in health checks, you gain a more granular and relevant understanding of your application's operational status. Whether you're monitoring external dependencies, internal components, or specific business logic, these custom indicators provide invaluable insights, enabling you to detect and resolve issues proactively. Embrace them to build more robust and observable Spring Boot applications!

Previous Post Next Post

Blog ads

ads