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 returnHealth.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!