Health Check Endpoint in Java

Introduction

Health Check endpoints are essential for monitoring application status, readiness, and liveness in modern distributed systems. They provide crucial information for load balancers, orchestration systems (like Kubernetes), and monitoring tools to determine if an application is functioning properly.

Basic Health Check Implementation

Simple Health Check Controller

@RestController
@RequestMapping("/health")
public class HealthCheckController {
private static final Logger logger = LoggerFactory.getLogger(HealthCheckController.class);
@GetMapping
public ResponseEntity<HealthResponse> healthCheck() {
HealthResponse health = new HealthResponse();
health.setStatus(HealthStatus.UP);
health.setTimestamp(Instant.now());
logger.debug("Health check executed - Status: UP");
return ResponseEntity.ok(health);
}
@GetMapping("/readiness")
public ResponseEntity<HealthResponse> readinessCheck() {
HealthResponse health = new HealthResponse();
try {
// Check if application is ready to receive traffic
if (isDatabaseConnected() && isExternalServiceAvailable()) {
health.setStatus(HealthStatus.UP);
logger.info("Readiness check passed");
} else {
health.setStatus(HealthStatus.DOWN);
health.setMessage("Application not ready to handle requests");
logger.warn("Readiness check failed");
}
} catch (Exception e) {
health.setStatus(HealthStatus.DOWN);
health.setMessage("Error during readiness check: " + e.getMessage());
logger.error("Readiness check error", e);
}
health.setTimestamp(Instant.now());
return health.getStatus() == HealthStatus.UP ? 
ResponseEntity.ok(health) : 
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(health);
}
@GetMapping("/liveness")
public ResponseEntity<HealthResponse> livenessCheck() {
HealthResponse health = new HealthResponse();
try {
// Check if application is alive and functioning
if (isApplicationAlive()) {
health.setStatus(HealthStatus.UP);
} else {
health.setStatus(HealthStatus.DOWN);
health.setMessage("Application is not alive");
logger.error("Liveness check failed");
}
} catch (Exception e) {
health.setStatus(HealthStatus.DOWN);
health.setMessage("Error during liveness check: " + e.getMessage());
logger.error("Liveness check error", e);
}
health.setTimestamp(Instant.now());
return health.getStatus() == HealthStatus.UP ? 
ResponseEntity.ok(health) : 
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(health);
}
private boolean isDatabaseConnected() {
// Implementation depends on your data source
return true; // Simplified for example
}
private boolean isExternalServiceAvailable() {
// Check external dependencies
return true; // Simplified for example
}
private boolean isApplicationAlive() {
// Basic application health checks
return true; // Simplified for example
}
}
// Health Response DTO
public class HealthResponse {
private HealthStatus status;
private String message;
private Instant timestamp;
private Map<String, Object> details;
// Constructors
public HealthResponse() {
this.details = new HashMap<>();
}
public HealthResponse(HealthStatus status, String message) {
this();
this.status = status;
this.message = message;
this.timestamp = Instant.now();
}
// Getters and Setters
public HealthStatus getStatus() { return status; }
public void setStatus(HealthStatus status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Instant getTimestamp() { return timestamp; }
public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; }
public Map<String, Object> getDetails() { return details; }
public void setDetails(Map<String, Object> details) { this.details = details; }
public void addDetail(String key, Object value) {
this.details.put(key, value);
}
}
public enum HealthStatus {
UP("UP"),
DOWN("DOWN"),
UNKNOWN("UNKNOWN"),
OUT_OF_SERVICE("OUT_OF_SERVICE");
private final String value;
HealthStatus(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

Comprehensive Health Check System

Health Indicator Interface

public interface HealthIndicator {
HealthResult check();
String getName();
HealthCheckType getType();
}
public class HealthResult {
private final HealthStatus status;
private final String message;
private final Map<String, Object> details;
private final Instant timestamp;
private final Throwable error;
public HealthResult(HealthStatus status, String message) {
this(status, message, null, null);
}
public HealthResult(HealthStatus status, String message, 
Map<String, Object> details, Throwable error) {
this.status = status;
this.message = message;
this.details = details != null ? details : new HashMap<>();
this.error = error;
this.timestamp = Instant.now();
}
// Getters
public HealthStatus getStatus() { return status; }
public String getMessage() { return message; }
public Map<String, Object> getDetails() { return details; }
public Instant getTimestamp() { return timestamp; }
public Throwable getError() { return error; }
public boolean isHealthy() {
return status == HealthStatus.UP;
}
// Builder pattern
public static Builder builder() {
return new Builder();
}
public static class Builder {
private HealthStatus status;
private String message;
private Map<String, Object> details = new HashMap<>();
private Throwable error;
public Builder status(HealthStatus status) {
this.status = status;
return this;
}
public Builder message(String message) {
this.message = message;
return this;
}
public Builder detail(String key, Object value) {
this.details.put(key, value);
return this;
}
public Builder error(Throwable error) {
this.error = error;
return this;
}
public HealthResult build() {
return new HealthResult(status, message, details, error);
}
}
}
public enum HealthCheckType {
READINESS("readiness"),
LIVENESS("liveness"),
STARTUP("startup"),
CUSTOM("custom");
private final String value;
HealthCheckType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

Database Health Indicator

@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private static final Logger logger = LoggerFactory.getLogger(DatabaseHealthIndicator.class);
private final DataSource dataSource;
private final JdbcTemplate jdbcTemplate;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public HealthResult check() {
try {
// Test database connection with a simple query
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
// Get connection metrics
int activeConnections = getActiveConnections();
int maxConnections = getMaxConnections();
double connectionUsage = maxConnections > 0 ? 
(double) activeConnections / maxConnections * 100 : 0;
return HealthResult.builder()
.status(HealthStatus.UP)
.message("Database is accessible")
.detail("activeConnections", activeConnections)
.detail("maxConnections", maxConnections)
.detail("connectionUsagePercent", Math.round(connectionUsage * 100.0) / 100.0)
.build();
} catch (Exception e) {
logger.error("Database health check failed", e);
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("Database is not accessible: " + e.getMessage())
.error(e)
.build();
}
}
@Override
public String getName() {
return "database";
}
@Override
public HealthCheckType getType() {
return HealthCheckType.READINESS;
}
private int getActiveConnections() {
// Implementation depends on connection pool
// This is a simplified example
try {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
return hikariDataSource.getHikariPoolMXBean().getActiveConnections();
}
} catch (Exception e) {
logger.debug("Could not get active connections count", e);
}
return -1; // Unknown
}
private int getMaxConnections() {
try {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
return hikariDataSource.getMaximumPoolSize();
}
} catch (Exception e) {
logger.debug("Could not get max connections", e);
}
return -1; // Unknown
}
}

Disk Space Health Indicator

@Component
public class DiskSpaceHealthIndicator implements HealthIndicator {
private static final Logger logger = LoggerFactory.getLogger(DiskSpaceHealthIndicator.class);
private final File path;
private final long thresholdBytes;
public DiskSpaceHealthIndicator(@Value("${health.disk.path:.}") String path,
@Value("${health.disk.threshold:10485760}") long thresholdBytes) {
this.path = new File(path);
this.thresholdBytes = thresholdBytes; // Default 10MB
}
@Override
public HealthResult check() {
try {
long freeSpace = path.getFreeSpace();
long totalSpace = path.getTotalSpace();
long usableSpace = path.getUsableSpace();
double freePercent = totalSpace > 0 ? (double) freeSpace / totalSpace * 100 : 0;
HealthStatus status = freeSpace >= thresholdBytes ? HealthStatus.UP : HealthStatus.DOWN;
String message = status == HealthStatus.UP ? 
"Disk space is sufficient" : 
"Disk space is running low";
return HealthResult.builder()
.status(status)
.message(message)
.detail("path", path.getAbsolutePath())
.detail("freeBytes", freeSpace)
.detail("totalBytes", totalSpace)
.detail("usableBytes", usableSpace)
.detail("freePercent", Math.round(freePercent * 100.0) / 100.0)
.detail("thresholdBytes", thresholdBytes)
.build();
} catch (Exception e) {
logger.error("Disk space health check failed", e);
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("Unable to check disk space: " + e.getMessage())
.error(e)
.build();
}
}
@Override
public String getName() {
return "diskSpace";
}
@Override
public HealthCheckType getType() {
return HealthCheckType.LIVENESS;
}
}

External Service Health Indicator

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private static final Logger logger = LoggerFactory.getLogger(ExternalServiceHealthIndicator.class);
private final RestTemplate restTemplate;
private final String serviceUrl;
private final String healthEndpoint;
private final long timeoutMs;
public ExternalServiceHealthIndicator(RestTemplate restTemplate,
@Value("${external.service.url}") String serviceUrl,
@Value("${external.service.health.endpoint:/health}") String healthEndpoint,
@Value("${external.service.timeout:5000}") long timeoutMs) {
this.restTemplate = restTemplate;
this.serviceUrl = serviceUrl;
this.healthEndpoint = healthEndpoint;
this.timeoutMs = timeoutMs;
}
@Override
public HealthResult check() {
String fullUrl = serviceUrl + healthEndpoint;
long startTime = System.currentTimeMillis();
try {
ResponseEntity<Map> response = restTemplate.getForEntity(fullUrl, Map.class);
long responseTime = System.currentTimeMillis() - startTime;
if (response.getStatusCode().is2xxSuccessful()) {
Map<String, Object> responseBody = response.getBody();
String status = responseBody != null ? 
(String) responseBody.get("status") : "UNKNOWN";
return HealthResult.builder()
.status("UP".equalsIgnoreCase(status) ? HealthStatus.UP : HealthStatus.DOWN)
.message("External service is " + status)
.detail("url", fullUrl)
.detail("responseTimeMs", responseTime)
.detail("httpStatus", response.getStatusCodeValue())
.detail("responseBody", responseBody)
.build();
} else {
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("External service returned HTTP " + response.getStatusCodeValue())
.detail("url", fullUrl)
.detail("responseTimeMs", responseTime)
.detail("httpStatus", response.getStatusCodeValue())
.build();
}
} catch (ResourceAccessException e) {
if (e.getCause() instanceof SocketTimeoutException) {
long responseTime = System.currentTimeMillis() - startTime;
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("External service timeout after " + responseTime + "ms")
.detail("url", fullUrl)
.detail("timeoutMs", timeoutMs)
.detail("responseTimeMs", responseTime)
.error(e)
.build();
} else {
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("External service connection failed: " + e.getMessage())
.detail("url", fullUrl)
.error(e)
.build();
}
} catch (Exception e) {
logger.error("External service health check failed", e);
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("External service check failed: " + e.getMessage())
.detail("url", fullUrl)
.error(e)
.build();
}
}
@Override
public String getName() {
return "externalService";
}
@Override
public HealthCheckType getType() {
return HealthCheckType.READINESS;
}
}

Health Check Registry and Service

Health Check Registry

@Component
public class HealthCheckRegistry {
private final Map<String, HealthIndicator> healthIndicators;
private final Map<HealthCheckType, List<HealthIndicator>> indicatorsByType;
public HealthCheckRegistry(List<HealthIndicator> indicators) {
this.healthIndicators = new ConcurrentHashMap<>();
this.indicatorsByType = new ConcurrentHashMap<>();
// Initialize maps
for (HealthCheckType type : HealthCheckType.values()) {
indicatorsByType.put(type, new CopyOnWriteArrayList<>());
}
// Register all indicators
if (indicators != null) {
indicators.forEach(this::register);
}
}
public void register(HealthIndicator indicator) {
healthIndicators.put(indicator.getName(), indicator);
indicatorsByType.get(indicator.getType()).add(indicator);
}
public void unregister(String name) {
HealthIndicator indicator = healthIndicators.remove(name);
if (indicator != null) {
indicatorsByType.get(indicator.getType()).remove(indicator);
}
}
public HealthResult checkHealth(String indicatorName) {
HealthIndicator indicator = healthIndicators.get(indicatorName);
if (indicator == null) {
return HealthResult.builder()
.status(HealthStatus.UNKNOWN)
.message("Health indicator not found: " + indicatorName)
.build();
}
return indicator.check();
}
public Map<String, HealthResult> checkAllHealth() {
Map<String, HealthResult> results = new ConcurrentHashMap<>();
healthIndicators.entrySet().parallelStream().forEach(entry -> {
String name = entry.getKey();
HealthIndicator indicator = entry.getValue();
results.put(name, indicator.check());
});
return results;
}
public Map<String, HealthResult> checkHealthByType(HealthCheckType type) {
List<HealthIndicator> indicators = indicatorsByType.get(type);
Map<String, HealthResult> results = new ConcurrentHashMap<>();
if (indicators != null) {
indicators.parallelStream().forEach(indicator -> {
results.put(indicator.getName(), indicator.check());
});
}
return results;
}
public HealthAggregate checkAggregateHealth(HealthCheckType type) {
Map<String, HealthResult> results = checkHealthByType(type);
return aggregateResults(results, type);
}
public HealthAggregate checkOverallHealth() {
Map<String, HealthResult> results = checkAllHealth();
return aggregateResults(results, HealthCheckType.CUSTOM);
}
private HealthAggregate aggregateResults(Map<String, HealthResult> results, HealthCheckType type) {
HealthStatus overallStatus = HealthStatus.UP;
Map<String, HealthResult> components = new HashMap<>();
List<String> errors = new ArrayList<>();
for (Map.Entry<String, HealthResult> entry : results.entrySet()) {
HealthResult result = entry.getValue();
components.put(entry.getKey(), result);
if (result.getStatus() == HealthStatus.DOWN) {
overallStatus = HealthStatus.DOWN;
if (result.getMessage() != null) {
errors.add(entry.getKey() + ": " + result.getMessage());
}
} else if (result.getStatus() == HealthStatus.UNKNOWN && overallStatus == HealthStatus.UP) {
overallStatus = HealthStatus.UNKNOWN;
}
}
return new HealthAggregate(overallStatus, type, components, errors);
}
}
public class HealthAggregate {
private final HealthStatus status;
private final HealthCheckType type;
private final Map<String, HealthResult> components;
private final List<String> errors;
private final Instant timestamp;
public HealthAggregate(HealthStatus status, HealthCheckType type, 
Map<String, HealthResult> components, List<String> errors) {
this.status = status;
this.type = type;
this.components = components != null ? components : new HashMap<>();
this.errors = errors != null ? errors : new ArrayList<>();
this.timestamp = Instant.now();
}
// Getters
public HealthStatus getStatus() { return status; }
public HealthCheckType getType() { return type; }
public Map<String, HealthResult> getComponents() { return components; }
public List<String> getErrors() { return errors; }
public Instant getTimestamp() { return timestamp; }
public boolean isHealthy() {
return status == HealthStatus.UP;
}
}

Advanced Health Check Controller

Comprehensive Health Check Endpoints

@RestController
@RequestMapping("/actuator/health")
public class ComprehensiveHealthCheckController {
private static final Logger logger = LoggerFactory.getLogger(ComprehensiveHealthCheckController.class);
private final HealthCheckRegistry healthRegistry;
private final ApplicationContext applicationContext;
public ComprehensiveHealthCheckController(HealthCheckRegistry healthRegistry,
ApplicationContext applicationContext) {
this.healthRegistry = healthRegistry;
this.applicationContext = applicationContext;
}
@GetMapping
public ResponseEntity<HealthResponse> overallHealth() {
HealthAggregate aggregate = healthRegistry.checkOverallHealth();
HealthResponse response = buildHealthResponse(aggregate);
HttpStatus httpStatus = aggregate.isHealthy() ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(httpStatus).body(response);
}
@GetMapping("/readiness")
public ResponseEntity<HealthResponse> readiness() {
HealthAggregate aggregate = healthRegistry.checkAggregateHealth(HealthCheckType.READINESS);
HealthResponse response = buildHealthResponse(aggregate);
HttpStatus httpStatus = aggregate.isHealthy() ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
logger.debug("Readiness check - Status: {}, Components: {}", 
aggregate.getStatus(), aggregate.getComponents().size());
return ResponseEntity.status(httpStatus).body(response);
}
@GetMapping("/liveness")
public ResponseEntity<HealthResponse> liveness() {
HealthAggregate aggregate = healthRegistry.checkAggregateHealth(HealthCheckType.LIVENESS);
HealthResponse response = buildHealthResponse(aggregate);
HttpStatus httpStatus = aggregate.isHealthy() ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(httpStatus).body(response);
}
@GetMapping("/startup")
public ResponseEntity<HealthResponse> startup() {
HealthAggregate aggregate = healthRegistry.checkAggregateHealth(HealthCheckType.STARTUP);
HealthResponse response = buildHealthResponse(aggregate);
HttpStatus httpStatus = aggregate.isHealthy() ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(httpStatus).body(response);
}
@GetMapping("/{indicator}")
public ResponseEntity<HealthResponse> indicatorHealth(@PathVariable String indicator) {
HealthResult result = healthRegistry.checkHealth(indicator);
HealthResponse response = new HealthResponse();
response.setStatus(result.getStatus());
response.setMessage(result.getMessage());
response.setTimestamp(result.getTimestamp());
response.setDetails(result.getDetails());
HttpStatus httpStatus = result.isHealthy() ? 
HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE;
return ResponseEntity.status(httpStatus).body(response);
}
@GetMapping("/components")
public ResponseEntity<Map<String, HealthResult>> allComponents() {
Map<String, HealthResult> results = healthRegistry.checkAllHealth();
return ResponseEntity.ok(results);
}
@GetMapping("/components/{type}")
public ResponseEntity<Map<String, HealthResult>> componentsByType(
@PathVariable HealthCheckType type) {
Map<String, HealthResult> results = healthRegistry.checkHealthByType(type);
return ResponseEntity.ok(results);
}
// Application info endpoint
@GetMapping("/info")
public ResponseEntity<ApplicationInfo> applicationInfo() {
ApplicationInfo info = new ApplicationInfo();
info.setName(applicationContext.getId());
info.setVersion(getApplicationVersion());
info.setEnvironment(getActiveProfile());
info.setStartTime(ManagementFactory.getRuntimeMXBean().getStartTime());
info.setUptime(ManagementFactory.getRuntimeMXBean().getUptime());
return ResponseEntity.ok(info);
}
private HealthResponse buildHealthResponse(HealthAggregate aggregate) {
HealthResponse response = new HealthResponse();
response.setStatus(aggregate.getStatus());
response.setTimestamp(aggregate.getTimestamp());
if (!aggregate.getErrors().isEmpty()) {
response.setMessage(String.join("; ", aggregate.getErrors()));
}
// Add component details
Map<String, Object> details = new HashMap<>();
aggregate.getComponents().forEach((name, result) -> {
Map<String, Object> componentDetail = new HashMap<>();
componentDetail.put("status", result.getStatus());
componentDetail.put("message", result.getMessage());
componentDetail.put("timestamp", result.getTimestamp());
if (result.getDetails() != null) {
componentDetail.putAll(result.getDetails());
}
details.put(name, componentDetail);
});
response.setDetails(details);
return response;
}
private String getApplicationVersion() {
try {
Package pkg = getClass().getPackage();
return pkg.getImplementationVersion() != null ? 
pkg.getImplementationVersion() : "unknown";
} catch (Exception e) {
return "unknown";
}
}
private String getActiveProfile() {
String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
return profiles.length > 0 ? profiles[0] : "default";
}
}
public class ApplicationInfo {
private String name;
private String version;
private String environment;
private long startTime;
private long uptime;
private Map<String, String> properties;
public ApplicationInfo() {
this.properties = new HashMap<>();
}
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public long getStartTime() { return startTime; }
public void setStartTime(long startTime) { this.startTime = startTime; }
public long getUptime() { return uptime; }
public void setUptime(long uptime) { this.uptime = uptime; }
public Map<String, String> getProperties() { return properties; }
public void setProperties(Map<String, String> properties) { this.properties = properties; }
public void addProperty(String key, String value) {
this.properties.put(key, value);
}
}

Custom Health Indicators

Cache Health Indicator

@Component
public class CacheHealthIndicator implements HealthIndicator {
private final CacheManager cacheManager;
public CacheHealthIndicator(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public HealthResult check() {
try {
Collection<String> cacheNames = cacheManager.getCacheNames();
Map<String, Object> cacheStats = new HashMap<>();
for (String cacheName : cacheNames) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
// Get cache statistics if available
Object nativeCache = cache.getNativeCache();
cacheStats.put(cacheName, getCacheInfo(nativeCache));
}
}
return HealthResult.builder()
.status(HealthStatus.UP)
.message("Cache system is healthy")
.detail("cacheCount", cacheNames.size())
.detail("caches", cacheStats)
.build();
} catch (Exception e) {
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("Cache system is not healthy: " + e.getMessage())
.error(e)
.build();
}
}
@Override
public String getName() {
return "cache";
}
@Override
public HealthCheckType getType() {
return HealthCheckType.READINESS;
}
private Map<String, Object> getCacheInfo(Object nativeCache) {
Map<String, Object> info = new HashMap<>();
if (nativeCache instanceof net.sf.ehcache.Ehcache) {
net.sf.ehcache.Ehcache ehcache = (net.sf.ehcache.Ehcache) nativeCache;
info.put("size", ehcache.getSize());
info.put("memoryStoreSize", ehcache.getMemoryStoreSize());
info.put("diskStoreSize", ehcache.getDiskStoreSize());
}
// Add other cache implementations as needed
return info;
}
}

Message Queue Health Indicator

@Component
public class MessageQueueHealthIndicator implements HealthIndicator {
private final JmsTemplate jmsTemplate;
private final String queueName;
public MessageQueueHealthIndicator(JmsTemplate jmsTemplate,
@Value("${app.messaging.queue.health:HEALTH_CHECK}") String queueName) {
this.jmsTemplate = jmsTemplate;
this.queueName = queueName;
}
@Override
public HealthResult check() {
long startTime = System.currentTimeMillis();
try {
// Test JMS connection by sending and receiving a test message
String testMessage = "Health check - " + Instant.now();
jmsTemplate.convertAndSend(queueName, testMessage);
// Try to receive the message (with timeout)
String receivedMessage = (String) jmsTemplate.receiveAndConvert(queueName);
long responseTime = System.currentTimeMillis() - startTime;
if (testMessage.equals(receivedMessage)) {
return HealthResult.builder()
.status(HealthStatus.UP)
.message("Message queue is responsive")
.detail("queueName", queueName)
.detail("responseTimeMs", responseTime)
.build();
} else {
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("Message queue test failed - message mismatch")
.detail("queueName", queueName)
.detail("responseTimeMs", responseTime)
.build();
}
} catch (Exception e) {
long responseTime = System.currentTimeMillis() - startTime;
return HealthResult.builder()
.status(HealthStatus.DOWN)
.message("Message queue is not accessible: " + e.getMessage())
.detail("queueName", queueName)
.detail("responseTimeMs", responseTime)
.error(e)
.build();
}
}
@Override
public String getName() {
return "messageQueue";
}
@Override
public HealthCheckType getType() {
return HealthCheckType.READINESS;
}
}

Configuration and Security

Health Check Configuration

@Configuration
@EnableConfigurationProperties(HealthCheckProperties.class)
public class HealthCheckConfig {
@Bean
public HealthCheckRegistry healthCheckRegistry(List<HealthIndicator> indicators) {
return new HealthCheckRegistry(indicators);
}
@Bean
public RestTemplate healthCheckRestTemplate(HealthCheckProperties properties) {
RestTemplate restTemplate = new RestTemplate();
// Configure timeouts for health checks
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(properties.getConnectTimeout());
factory.setReadTimeout(properties.getReadTimeout());
restTemplate.setRequestFactory(factory);
return restTemplate;
}
@Bean
@ConditionalOnMissingBean
public HealthIndicator applicationHealthIndicator() {
return new ApplicationHealthIndicator();
}
}
@ConfigurationProperties(prefix = "health.check")
@Data
public class HealthCheckProperties {
private boolean enabled = true;
private int connectTimeout = 5000;
private int readTimeout = 10000;
private String path = "/actuator/health";
private Cache cache = new Cache();
private Disk disk = new Disk();
@Data
public static class Cache {
private boolean enabled = true;
private int timeout = 3000;
}
@Data
public static class Disk {
private String path = ".";
private long thresholdBytes = 10485760L; // 10MB
}
}

Security Configuration for Health Endpoints

@Configuration
@EnableWebSecurity
public class HealthSecurityConfig {
@Value("${management.endpoint.health.roles:ACTUATOR}")
private String healthEndpointRoles;
@Bean
public SecurityFilterChain healthSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/health/**")
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health", "/actuator/health/liveness").permitAll()
.requestMatchers("/actuator/health/**").hasAnyRole(healthEndpointRoles.split(","))
)
.httpBasic(withDefaults())
.csrf(csrf -> csrf.ignoringRequestMatchers("/actuator/health/**"));
return http.build();
}
}

Testing Health Checks

Health Check Tests

@SpringBootTest
@AutoConfigureTestDatabase
class HealthCheckIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private HealthCheckRegistry healthRegistry;
@MockBean
private DataSource dataSource;
@Test
void overallHealthCheck_ShouldReturnUp_WhenAllComponentsHealthy() {
ResponseEntity<HealthResponse> response = restTemplate.getForEntity(
"/actuator/health", HealthResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getStatus()).isEqualTo(HealthStatus.UP);
}
@Test
void readinessCheck_ShouldReturnDetails_WhenRequested() {
ResponseEntity<HealthResponse> response = restTemplate.getForEntity(
"/actuator/health/readiness", HealthResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getDetails()).isNotEmpty();
}
@Test
void specificIndicator_ShouldReturnStatus() {
ResponseEntity<HealthResponse> response = restTemplate.getForEntity(
"/actuator/health/database", HealthResponse.class);
assertThat(response.getStatusCode()).isIn(HttpStatus.OK, HttpStatus.SERVICE_UNAVAILABLE);
assertThat(response.getBody().getStatus()).isNotNull();
}
@Test
void whenDatabaseDown_ReadinessShouldReturnDown() throws SQLException {
// Mock database failure
when(dataSource.getConnection()).thenThrow(new SQLException("Connection failed"));
HealthResult result = healthRegistry.checkHealth("database");
assertThat(result.getStatus()).isEqualTo(HealthStatus.DOWN);
assertThat(result.getMessage()).contains("Connection failed");
}
}
@Component
public class TestHealthIndicator implements HealthIndicator {
private HealthStatus testStatus = HealthStatus.UP;
@Override
public HealthResult check() {
return HealthResult.builder()
.status(testStatus)
.message("Test health indicator")
.detail("testData", "testValue")
.build();
}
@Override
public String getName() {
return "test";
}
@Override
public HealthCheckType getType() {
return HealthCheckType.CUSTOM;
}
public void setTestStatus(HealthStatus status) {
this.testStatus = status;
}
}

This comprehensive Health Check Endpoint implementation provides robust monitoring capabilities for Java applications, suitable for use in containerized environments, microservices architectures, and traditional deployments. The system is extensible, secure, and provides detailed insights into application health.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper