mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-05-20 10:24:53 +00:00
* Add Health Check pattern implementation The commit introduces Health Check pattern, providing a series of health indicators for system performance and stability monitoring, including checks for system CPU load, process CPU load, database health, memory usage, and garbage collection metrics. It also includes asynchronous execution and caching mechanisms for health checks, and retry configurations for resilience. Implements health checking components as per issue #2695. * Test cases and javadoc for HealthEndpointIntegrationTest * Added more log to test case to see why it returns 503 * Change config values to see if the system High system CPU load is resolved or not in CI. * Fixes for test cases. * some fixes for Sonar. * some fixes for Sonar. ADDED HIGH_PROCESS_CPU_LOAD_MESSAGE_WITHOUT_PARAM ADDED HIGH_SYSTEM_CPU_LOAD_MESSAGE_WITHOUT_PARAM * Sonar fixes address "Define and throw a dedicated exception instead of using a generic one." added HealthCheckInterruptedException refactored CustomHealthIndicator * fixes checkstyle violation.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Health Check Pattern
|
||||
category: Performance
|
||||
language: en
|
||||
tag:
|
||||
- Microservices
|
||||
- Resilience
|
||||
- Observability
|
||||
---
|
||||
|
||||
# Health Check Pattern
|
||||
|
||||
## Also known as
|
||||
Health Monitoring, Service Health Check
|
||||
|
||||
## Intent
|
||||
To ensure the stability and resilience of services in a microservices architecture by providing a way to monitor and diagnose their health.
|
||||
|
||||
## Explanation
|
||||
In microservices architecture, it's critical to continuously check the health of individual services. The Health Check Pattern is a mechanism for microservices to expose their health status. This pattern is implemented by including a health check endpoint in microservices that returns the service's current state. This is vital for maintaining system resilience and operational readiness.
|
||||
|
||||
## Class Diagram
|
||||

|
||||
|
||||
## Applicability
|
||||
Use the Health Check Pattern when:
|
||||
- You have an application composed of multiple services and need to monitor the health of each service individually.
|
||||
- You want to implement automatic service recovery or replacement based on health status.
|
||||
- You are employing orchestration or automation tools that rely on health checks to manage service instances.
|
||||
|
||||
## Tutorials
|
||||
- Implementing Health Checks in Java using Spring Boot Actuator.
|
||||
|
||||
## Known Uses
|
||||
- Kubernetes Liveness and Readiness Probes
|
||||
- AWS Elastic Load Balancing Health Checks
|
||||
- Spring Boot Actuator
|
||||
|
||||
## Consequences
|
||||
**Pros:**
|
||||
- Enhances the fault tolerance of the system by detecting failures and enabling quick recovery.
|
||||
- Improves the visibility of system health for operational monitoring and alerting.
|
||||
|
||||
**Cons:**
|
||||
- Adds complexity to service implementation.
|
||||
- Requires a strategy to handle cascading failures when dependent services are unhealthy.
|
||||
|
||||
## Related Patterns
|
||||
- Circuit Breaker
|
||||
- Retry Pattern
|
||||
- Timeout Pattern
|
||||
|
||||
## Credits
|
||||
Inspired by the Health Check API pattern from [microservices.io](https://microservices.io/patterns/observability/health-check-api.html) and the issue [#2695](https://github.com/iluwatar/java-design-patterns/issues/2695) on iluwatar's Java design patterns repository.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@@ -0,0 +1,90 @@
|
||||
@startuml
|
||||
|
||||
!theme plain
|
||||
top to bottom direction
|
||||
skinparam linetype ortho
|
||||
|
||||
class App {
|
||||
+ App():
|
||||
+ main(String[]): void
|
||||
}
|
||||
class AsynchronousHealthChecker {
|
||||
+ AsynchronousHealthChecker():
|
||||
+ performCheck(Supplier<Health>, long): CompletableFuture<Health>
|
||||
- awaitTerminationWithTimeout(): boolean
|
||||
+ shutdown(): void
|
||||
}
|
||||
class CpuHealthIndicator {
|
||||
+ CpuHealthIndicator():
|
||||
- processCpuLoadThreshold: double
|
||||
- systemCpuLoadThreshold: double
|
||||
- loadAverageThreshold: double
|
||||
- osBean: OperatingSystemMXBean
|
||||
- defaultWarningMessage: String
|
||||
+ init(): void
|
||||
+ health(): Health
|
||||
defaultWarningMessage: String
|
||||
osBean: OperatingSystemMXBean
|
||||
loadAverageThreshold: double
|
||||
processCpuLoadThreshold: double
|
||||
systemCpuLoadThreshold: double
|
||||
}
|
||||
class CustomHealthIndicator {
|
||||
+ CustomHealthIndicator(AsynchronousHealthChecker, CacheManager, HealthCheckRepository):
|
||||
+ evictHealthCache(): void
|
||||
- check(): Health
|
||||
+ health(): Health
|
||||
}
|
||||
class DatabaseTransactionHealthIndicator {
|
||||
+ DatabaseTransactionHealthIndicator(HealthCheckRepository, AsynchronousHealthChecker, RetryTemplate):
|
||||
- retryTemplate: RetryTemplate
|
||||
- healthCheckRepository: HealthCheckRepository
|
||||
- timeoutInSeconds: long
|
||||
- asynchronousHealthChecker: AsynchronousHealthChecker
|
||||
+ health(): Health
|
||||
timeoutInSeconds: long
|
||||
retryTemplate: RetryTemplate
|
||||
healthCheckRepository: HealthCheckRepository
|
||||
asynchronousHealthChecker: AsynchronousHealthChecker
|
||||
}
|
||||
class GarbageCollectionHealthIndicator {
|
||||
+ GarbageCollectionHealthIndicator():
|
||||
- memoryUsageThreshold: double
|
||||
+ health(): Health
|
||||
memoryPoolMxBeans: List<MemoryPoolMXBean>
|
||||
garbageCollectorMxBeans: List<GarbageCollectorMXBean>
|
||||
memoryUsageThreshold: double
|
||||
}
|
||||
class HealthCheck {
|
||||
+ HealthCheck():
|
||||
- status: String
|
||||
- id: Integer
|
||||
+ equals(Object): boolean
|
||||
# canEqual(Object): boolean
|
||||
+ hashCode(): int
|
||||
+ toString(): String
|
||||
id: Integer
|
||||
status: String
|
||||
}
|
||||
class HealthCheckRepository {
|
||||
+ HealthCheckRepository():
|
||||
+ performTestTransaction(): void
|
||||
+ checkHealth(): Integer
|
||||
}
|
||||
class MemoryHealthIndicator {
|
||||
+ MemoryHealthIndicator(AsynchronousHealthChecker):
|
||||
+ checkMemory(): Health
|
||||
+ health(): Health
|
||||
}
|
||||
class RetryConfig {
|
||||
+ RetryConfig():
|
||||
+ retryTemplate(): RetryTemplate
|
||||
}
|
||||
|
||||
CustomHealthIndicator "1" *-[#595959,plain]-> "healthChecker\n1" AsynchronousHealthChecker
|
||||
CustomHealthIndicator "1" *-[#595959,plain]-> "healthCheckRepository\n1" HealthCheckRepository
|
||||
DatabaseTransactionHealthIndicator "1" *-[#595959,plain]-> "asynchronousHealthChecker\n1" AsynchronousHealthChecker
|
||||
DatabaseTransactionHealthIndicator "1" *-[#595959,plain]-> "healthCheckRepository\n1" HealthCheckRepository
|
||||
HealthCheckRepository -[#595959,dashed]-> HealthCheck : "«create»"
|
||||
MemoryHealthIndicator "1" *-[#595959,plain]-> "asynchronousHealthChecker\n1" AsynchronousHealthChecker
|
||||
@enduml
|
||||
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>health-check</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Actuator for health check -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Data JPA for database access -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Retry for retrying failed database operations -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- JUnit Jupiter Engine for testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Mockito for mocking in tests -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- H2 for mocking database -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.24.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Maven Assembly Plugin for creating a single distributable JAR -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<archive>
|
||||
<manifest>
|
||||
<!-- Define the main class for the executable JAR -->
|
||||
<mainClass>com.iluwatar.healthcheck.App</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Other plugins as needed -->
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* This application provides health check APIs for various aspects of the microservice architecture,
|
||||
* including database transactions, garbage collection, and overall system health. These health
|
||||
* checks are essential for monitoring the health and performance of the microservices and ensuring
|
||||
* their availability and responsiveness. For more information about health checks and their role in
|
||||
* microservice architectures, please refer to: [Microservices Health Checks
|
||||
* API]('https://microservices.io/patterns/observability/health-check-api.html').
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@EnableCaching
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
public class App {
|
||||
/** Program entry point. */
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Supplier;
|
||||
import javax.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* An asynchronous health checker component that executes health checks in a separate thread.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AsynchronousHealthChecker {
|
||||
|
||||
/** A scheduled executor service used to execute health checks in a separate thread. */
|
||||
private final ScheduledExecutorService healthCheckExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private static final String HEALTH_CHECK_TIMEOUT_MESSAGE = "Health check timed out";
|
||||
private static final String HEALTH_CHECK_FAILED_MESSAGE = "Health check failed";
|
||||
|
||||
/**
|
||||
* Performs a health check asynchronously using the provided health check logic with a specified
|
||||
* timeout.
|
||||
*
|
||||
* @param healthCheck the health check logic supplied as a {@code Supplier<Health>}
|
||||
* @param timeoutInSeconds the maximum time to wait for the health check to complete, in seconds
|
||||
* @return a {@code CompletableFuture<Health>} object that represents the result of the health
|
||||
* check
|
||||
*/
|
||||
public CompletableFuture<Health> performCheck(
|
||||
Supplier<Health> healthCheck, long timeoutInSeconds) {
|
||||
CompletableFuture<Health> future =
|
||||
CompletableFuture.supplyAsync(healthCheck, healthCheckExecutor);
|
||||
|
||||
// Schedule a task to enforce the timeout
|
||||
healthCheckExecutor.schedule(
|
||||
() -> {
|
||||
if (!future.isDone()) {
|
||||
LOGGER.error(HEALTH_CHECK_TIMEOUT_MESSAGE);
|
||||
future.completeExceptionally(new TimeoutException(HEALTH_CHECK_TIMEOUT_MESSAGE));
|
||||
}
|
||||
},
|
||||
timeoutInSeconds,
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
return future.handle(
|
||||
(result, throwable) -> {
|
||||
if (throwable != null) {
|
||||
LOGGER.error(HEALTH_CHECK_FAILED_MESSAGE, throwable);
|
||||
// Check if the throwable is a TimeoutException or caused by a TimeoutException
|
||||
Throwable rootCause =
|
||||
throwable instanceof CompletionException ? throwable.getCause() : throwable;
|
||||
if (!(rootCause instanceof TimeoutException)) {
|
||||
LOGGER.error(HEALTH_CHECK_FAILED_MESSAGE, rootCause);
|
||||
return Health.down().withException(rootCause).build();
|
||||
} else {
|
||||
LOGGER.error(HEALTH_CHECK_TIMEOUT_MESSAGE, rootCause);
|
||||
// If it is a TimeoutException, rethrow it wrapped in a CompletionException
|
||||
throw new CompletionException(rootCause);
|
||||
}
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the health check executor service has terminated completely. This method waits
|
||||
* for the executor service to finish all its tasks within a specified timeout. If the timeout is
|
||||
* reached before all tasks are completed, the method returns `true`; otherwise, it returns
|
||||
* `false`.
|
||||
*
|
||||
* @throws InterruptedException if the current thread is interrupted while waiting for the
|
||||
* executor service to terminate
|
||||
*/
|
||||
private boolean awaitTerminationWithTimeout() throws InterruptedException {
|
||||
boolean isTerminationIncomplete = !healthCheckExecutor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
LOGGER.info("Termination status: {}", isTerminationIncomplete);
|
||||
// Await termination and return true if termination is incomplete (timeout elapsed)
|
||||
return isTerminationIncomplete;
|
||||
}
|
||||
|
||||
/** Shuts down the executor service, allowing in-flight tasks to complete. */
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
try {
|
||||
// Wait a while for existing tasks to terminate
|
||||
if (awaitTerminationWithTimeout()) {
|
||||
LOGGER.info("Health check executor did not terminate in time");
|
||||
// Attempt to cancel currently executing tasks
|
||||
healthCheckExecutor.shutdownNow();
|
||||
// Wait again for tasks to respond to being cancelled
|
||||
if (awaitTerminationWithTimeout()) {
|
||||
LOGGER.error("Health check executor did not terminate");
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
// Preserve interrupt status
|
||||
Thread.currentThread().interrupt();
|
||||
// (Re-)Cancel if current thread also interrupted
|
||||
healthCheckExecutor.shutdownNow();
|
||||
// Log the stack trace for interrupted exception
|
||||
LOGGER.error("Shutdown of the health check executor was interrupted", ie);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.annotation.PostConstruct;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A health indicator that checks the health of the system's CPU.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CpuHealthIndicator implements HealthIndicator {
|
||||
|
||||
/** The operating system MXBean used to gather CPU health information. */
|
||||
private OperatingSystemMXBean osBean;
|
||||
|
||||
/** Initializes the {@link OperatingSystemMXBean} instance. */
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* The system CPU load threshold. If the system CPU load is above this threshold, the health
|
||||
* indicator will return a `down` health status.
|
||||
*/
|
||||
@Value("${cpu.system.load.threshold:80.0}")
|
||||
private double systemCpuLoadThreshold;
|
||||
|
||||
/**
|
||||
* The process CPU load threshold. If the process CPU load is above this threshold, the health
|
||||
* indicator will return a `down` health status.
|
||||
*/
|
||||
@Value("${cpu.process.load.threshold:50.0}")
|
||||
private double processCpuLoadThreshold;
|
||||
|
||||
/**
|
||||
* The load average threshold. If the load average is above this threshold, the health indicator
|
||||
* will return an `up` health status with a warning message.
|
||||
*/
|
||||
@Value("${cpu.load.average.threshold:0.75}")
|
||||
private double loadAverageThreshold;
|
||||
|
||||
/**
|
||||
* The warning message to include in the health indicator's response when the load average is high
|
||||
* but not exceeding the threshold.
|
||||
*/
|
||||
@Value("${cpu.warning.message:High load average}")
|
||||
private String defaultWarningMessage;
|
||||
|
||||
private static final String ERROR_MESSAGE = "error";
|
||||
|
||||
private static final String HIGH_SYSTEM_CPU_LOAD_MESSAGE = "High system CPU load: {}";
|
||||
private static final String HIGH_PROCESS_CPU_LOAD_MESSAGE = "High process CPU load: {}";
|
||||
private static final String HIGH_LOAD_AVERAGE_MESSAGE = "High load average: {}";
|
||||
private static final String HIGH_PROCESS_CPU_LOAD_MESSAGE_WITHOUT_PARAM = "High process CPU load";
|
||||
private static final String HIGH_SYSTEM_CPU_LOAD_MESSAGE_WITHOUT_PARAM = "High system CPU load";
|
||||
private static final String HIGH_LOAD_AVERAGE_MESSAGE_WITHOUT_PARAM = "High load average";
|
||||
|
||||
/**
|
||||
* Checks the health of the system's CPU and returns a health indicator object.
|
||||
*
|
||||
* @return a health indicator object
|
||||
*/
|
||||
@Override
|
||||
public Health health() {
|
||||
|
||||
if (!(osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean)) {
|
||||
LOGGER.error("Unsupported operating system MXBean: {}", osBean.getClass().getName());
|
||||
return Health.unknown()
|
||||
.withDetail(ERROR_MESSAGE, "Unsupported operating system MXBean")
|
||||
.build();
|
||||
}
|
||||
|
||||
double systemCpuLoad = sunOsBean.getCpuLoad() * 100;
|
||||
double processCpuLoad = sunOsBean.getProcessCpuLoad() * 100;
|
||||
int availableProcessors = sunOsBean.getAvailableProcessors();
|
||||
double loadAverage = sunOsBean.getSystemLoadAverage();
|
||||
|
||||
Map<String, Object> details = new HashMap<>();
|
||||
details.put("timestamp", Instant.now());
|
||||
details.put("systemCpuLoad", String.format("%.2f%%", systemCpuLoad));
|
||||
details.put("processCpuLoad", String.format("%.2f%%", processCpuLoad));
|
||||
details.put("availableProcessors", availableProcessors);
|
||||
details.put("loadAverage", loadAverage);
|
||||
|
||||
if (systemCpuLoad > systemCpuLoadThreshold) {
|
||||
LOGGER.error(HIGH_SYSTEM_CPU_LOAD_MESSAGE, systemCpuLoad);
|
||||
return Health.down()
|
||||
.withDetails(details)
|
||||
.withDetail(ERROR_MESSAGE, HIGH_SYSTEM_CPU_LOAD_MESSAGE_WITHOUT_PARAM)
|
||||
.build();
|
||||
} else if (processCpuLoad > processCpuLoadThreshold) {
|
||||
LOGGER.error(HIGH_PROCESS_CPU_LOAD_MESSAGE, processCpuLoad);
|
||||
return Health.down()
|
||||
.withDetails(details)
|
||||
.withDetail(ERROR_MESSAGE, HIGH_PROCESS_CPU_LOAD_MESSAGE_WITHOUT_PARAM)
|
||||
.build();
|
||||
} else if (loadAverage > (availableProcessors * loadAverageThreshold)) {
|
||||
LOGGER.error(HIGH_LOAD_AVERAGE_MESSAGE, loadAverage);
|
||||
return Health.up()
|
||||
.withDetails(details)
|
||||
.withDetail(ERROR_MESSAGE, HIGH_LOAD_AVERAGE_MESSAGE_WITHOUT_PARAM)
|
||||
.build();
|
||||
} else {
|
||||
return Health.up().withDetails(details).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A custom health indicator that periodically checks the health of a database and caches the
|
||||
* result. It leverages an asynchronous health checker to perform the health checks.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomHealthIndicator implements HealthIndicator {
|
||||
|
||||
private final AsynchronousHealthChecker healthChecker;
|
||||
private final CacheManager cacheManager;
|
||||
private final HealthCheckRepository healthCheckRepository;
|
||||
|
||||
@Value("${health.check.timeout:10}")
|
||||
private long timeoutInSeconds;
|
||||
|
||||
/**
|
||||
* Perform a health check and cache the result.
|
||||
*
|
||||
* @return the health status of the application
|
||||
* @throws HealthCheckInterruptedException if the health check is interrupted
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(value = "health-check", unless = "#result.status == 'DOWN'")
|
||||
public Health health() {
|
||||
LOGGER.info("Performing health check");
|
||||
CompletableFuture<Health> healthFuture =
|
||||
healthChecker.performCheck(this::check, timeoutInSeconds);
|
||||
try {
|
||||
return healthFuture.get(timeoutInSeconds, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOGGER.error("Health check interrupted", e);
|
||||
throw new HealthCheckInterruptedException(e);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Health check failed", e);
|
||||
return Health.down(e).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the health of the database by querying for a simple constant value expected from the
|
||||
* database.
|
||||
*
|
||||
* @return Health indicating UP if the database returns the constant correctly, otherwise DOWN.
|
||||
*/
|
||||
private Health check() {
|
||||
Integer result = healthCheckRepository.checkHealth();
|
||||
boolean databaseIsUp = result != null && result == 1;
|
||||
LOGGER.info("Health check result: {}", databaseIsUp);
|
||||
return databaseIsUp
|
||||
? Health.up().withDetail("database", "reachable").build()
|
||||
: Health.down().withDetail("database", "unreachable").build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all entries from the health check cache. This is scheduled to run at a fixed rate
|
||||
* defined in the application properties.
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${health.check.cache.evict.interval:60000}")
|
||||
public void evictHealthCache() {
|
||||
LOGGER.info("Evicting health check cache");
|
||||
try {
|
||||
Cache healthCheckCache = cacheManager.getCache("health-check");
|
||||
LOGGER.info("Health check cache: {}", healthCheckCache);
|
||||
if (healthCheckCache != null) {
|
||||
healthCheckCache.clear();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to evict health check cache", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A health indicator that checks the health of database transactions by attempting to perform a
|
||||
* test transaction using a retry mechanism. If the transaction succeeds after multiple attempts,
|
||||
* the health indicator returns {@link Health#up()} and logs a success message. If all retry
|
||||
* attempts fail, the health indicator returns {@link Health#down()} and logs an error message.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Setter
|
||||
@Getter
|
||||
public class DatabaseTransactionHealthIndicator implements HealthIndicator {
|
||||
|
||||
/** A repository for performing health checks on the database. */
|
||||
private final HealthCheckRepository healthCheckRepository;
|
||||
|
||||
/** An asynchronous health checker used to execute health checks in a separate thread. */
|
||||
private final AsynchronousHealthChecker asynchronousHealthChecker;
|
||||
|
||||
/** A retry template used to retry the test transaction if it fails due to a transient error. */
|
||||
private final RetryTemplate retryTemplate;
|
||||
|
||||
/**
|
||||
* The timeout in seconds for the health check. If the health check does not complete within this
|
||||
* timeout, it will be considered timed out and will return {@link Health#down()}.
|
||||
*/
|
||||
@Value("${health.check.timeout:10}")
|
||||
private long timeoutInSeconds;
|
||||
|
||||
/**
|
||||
* Performs a health check by attempting to perform a test transaction with retry support.
|
||||
*
|
||||
* @return the health status of the database transactions
|
||||
*/
|
||||
@Override
|
||||
public Health health() {
|
||||
LOGGER.info("Calling performCheck with timeout {}", timeoutInSeconds);
|
||||
Supplier<Health> dbTransactionCheck =
|
||||
() -> {
|
||||
try {
|
||||
healthCheckRepository.performTestTransaction();
|
||||
return Health.up().build();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Database transaction health check failed", e);
|
||||
return Health.down(e).build();
|
||||
}
|
||||
};
|
||||
try {
|
||||
return asynchronousHealthChecker.performCheck(dbTransactionCheck, timeoutInSeconds).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
LOGGER.error("Database transaction health check timed out or was interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
return Health.down(e).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A custom health indicator that checks the garbage collection status of the application and
|
||||
* reports the health status accordingly. It gathers information about the collection count,
|
||||
* collection time, memory pool name, and garbage collector algorithm for each garbage collector and
|
||||
* presents the details in a structured manner.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Getter
|
||||
@Setter
|
||||
public class GarbageCollectionHealthIndicator implements HealthIndicator {
|
||||
|
||||
/**
|
||||
* The memory usage threshold above which a warning message is included in the health check
|
||||
* report.
|
||||
*/
|
||||
@Value("${memory.usage.threshold:0.8}")
|
||||
private double memoryUsageThreshold;
|
||||
|
||||
/**
|
||||
* Performs a health check by gathering garbage collection metrics and evaluating the overall
|
||||
* health of the garbage collection system.
|
||||
*
|
||||
* @return a {@link Health} object representing the health status of the garbage collection system
|
||||
*/
|
||||
@Override
|
||||
public Health health() {
|
||||
List<GarbageCollectorMXBean> gcBeans = getGarbageCollectorMxBeans();
|
||||
List<MemoryPoolMXBean> memoryPoolMxBeans = getMemoryPoolMxBeans();
|
||||
Map<String, Map<String, String>> gcDetails = new HashMap<>();
|
||||
|
||||
for (GarbageCollectorMXBean gcBean : gcBeans) {
|
||||
Map<String, String> collectorDetails = createCollectorDetails(gcBean, memoryPoolMxBeans);
|
||||
gcDetails.put(gcBean.getName(), collectorDetails);
|
||||
}
|
||||
|
||||
return Health.up().withDetails(gcDetails).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates details for the given garbage collector, including collection count, collection time,
|
||||
* and memory pool information.
|
||||
*
|
||||
* @param gcBean The garbage collector MXBean
|
||||
* @param memoryPoolMxBeans List of memory pool MXBeans
|
||||
* @return Map containing details for the garbage collector
|
||||
*/
|
||||
private Map<String, String> createCollectorDetails(
|
||||
GarbageCollectorMXBean gcBean, List<MemoryPoolMXBean> memoryPoolMxBeans) {
|
||||
Map<String, String> collectorDetails = new HashMap<>();
|
||||
long count = gcBean.getCollectionCount();
|
||||
long time = gcBean.getCollectionTime();
|
||||
collectorDetails.put("count", String.format("%d", count));
|
||||
collectorDetails.put("time", String.format("%dms", time));
|
||||
|
||||
String[] memoryPoolNames = gcBean.getMemoryPoolNames();
|
||||
List<String> memoryPoolNamesList = Arrays.asList(memoryPoolNames);
|
||||
if (!memoryPoolNamesList.isEmpty()) {
|
||||
addMemoryPoolDetails(collectorDetails, memoryPoolMxBeans, memoryPoolNamesList);
|
||||
} else {
|
||||
LOGGER.error("Garbage collector '{}' does not have any memory pools", gcBean.getName());
|
||||
}
|
||||
|
||||
return collectorDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds memory pool details to the collector details.
|
||||
*
|
||||
* @param collectorDetails Map containing details for the garbage collector
|
||||
* @param memoryPoolMxBeans List of memory pool MXBeans
|
||||
* @param memoryPoolNamesList List of memory pool names associated with the garbage collector
|
||||
*/
|
||||
private void addMemoryPoolDetails(
|
||||
Map<String, String> collectorDetails,
|
||||
List<MemoryPoolMXBean> memoryPoolMxBeans,
|
||||
List<String> memoryPoolNamesList) {
|
||||
for (MemoryPoolMXBean memoryPoolmxbean : memoryPoolMxBeans) {
|
||||
if (memoryPoolNamesList.contains(memoryPoolmxbean.getName())) {
|
||||
double memoryUsage =
|
||||
memoryPoolmxbean.getUsage().getUsed() / (double) memoryPoolmxbean.getUsage().getMax();
|
||||
if (memoryUsage > memoryUsageThreshold) {
|
||||
collectorDetails.put(
|
||||
"warning",
|
||||
String.format(
|
||||
"Memory pool '%s' usage is high (%2f%%)",
|
||||
memoryPoolmxbean.getName(), memoryUsage));
|
||||
}
|
||||
|
||||
collectorDetails.put(
|
||||
"memoryPools", String.format("%s: %s%%", memoryPoolmxbean.getName(), memoryUsage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of garbage collector MXBeans using ManagementFactory.
|
||||
*
|
||||
* @return a list of {@link GarbageCollectorMXBean} objects representing the garbage collectors
|
||||
*/
|
||||
protected List<GarbageCollectorMXBean> getGarbageCollectorMxBeans() {
|
||||
return ManagementFactory.getGarbageCollectorMXBeans();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of memory pool MXBeans using ManagementFactory.
|
||||
*
|
||||
* @return a list of {@link MemoryPoolMXBean} objects representing the memory pools
|
||||
*/
|
||||
protected List<MemoryPoolMXBean> getMemoryPoolMxBeans() {
|
||||
return ManagementFactory.getMemoryPoolMXBeans();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* An entity class that represents a health check record in the database. This class is used to
|
||||
* persist the results of health checks performed by the `DatabaseTransactionHealthIndicator`.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Entity
|
||||
@Data
|
||||
public class HealthCheck {
|
||||
|
||||
/** The unique identifier of the health check record. */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
/** The status of the health check. Possible values are "UP" and "DOWN". */
|
||||
@Column(name = "status")
|
||||
private String status;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
/**
|
||||
* Exception thrown when the health check is interrupted during execution. This exception is a
|
||||
* runtime exception that wraps the original cause.
|
||||
*/
|
||||
public class HealthCheckInterruptedException extends RuntimeException {
|
||||
/**
|
||||
* Constructs a new HealthCheckInterruptedException with the specified cause.
|
||||
*
|
||||
* @param cause the cause of the exception
|
||||
*/
|
||||
public HealthCheckInterruptedException(Throwable cause) {
|
||||
super("Health check interrupted", cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.transaction.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* A repository class for managing health check records in the database. This class provides methods
|
||||
* for checking the health of the database connection and performing test transactions.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
public class HealthCheckRepository {
|
||||
|
||||
private static final String HEALTH_CHECK_OK = "OK";
|
||||
|
||||
@PersistenceContext private EntityManager entityManager;
|
||||
|
||||
/**
|
||||
* Checks the health of the database connection by executing a simple query that should always
|
||||
* return 1 if the connection is healthy.
|
||||
*
|
||||
* @return 1 if the database connection is healthy, or null otherwise
|
||||
*/
|
||||
public Integer checkHealth() {
|
||||
try {
|
||||
return (Integer) entityManager.createNativeQuery("SELECT 1").getSingleResult();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Health check query failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a test transaction by writing a record to the `health_check` table, reading it back,
|
||||
* and then deleting it. If any of these operations fail, an exception is thrown.
|
||||
*
|
||||
* @throws Exception if the test transaction fails
|
||||
*/
|
||||
@Transactional
|
||||
public void performTestTransaction() {
|
||||
try {
|
||||
HealthCheck healthCheck = new HealthCheck();
|
||||
healthCheck.setStatus(HEALTH_CHECK_OK);
|
||||
entityManager.persist(healthCheck);
|
||||
entityManager.flush();
|
||||
HealthCheck retrievedHealthCheck = entityManager.find(HealthCheck.class, healthCheck.getId());
|
||||
entityManager.remove(retrievedHealthCheck);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Test transaction failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A custom health indicator that checks the memory usage of the application and reports the health
|
||||
* status accordingly. It uses an asynchronous health checker to perform the health check and a
|
||||
* configurable memory usage threshold to determine the health status.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MemoryHealthIndicator implements HealthIndicator {
|
||||
|
||||
private final AsynchronousHealthChecker asynchronousHealthChecker;
|
||||
|
||||
/** The timeout in seconds for the health check. */
|
||||
@Value("${health.check.timeout:10}")
|
||||
private long timeoutInSeconds;
|
||||
|
||||
/**
|
||||
* The memory usage threshold in percentage. If the memory usage is less than this threshold, the
|
||||
* health status is reported as UP. Otherwise, the health status is reported as DOWN.
|
||||
*/
|
||||
@Value("${health.check.memory.threshold:0.9}")
|
||||
private double memoryThreshold;
|
||||
|
||||
/**
|
||||
* Performs a health check by checking the memory usage of the application.
|
||||
*
|
||||
* @return the health status of the application
|
||||
*/
|
||||
public Health checkMemory() {
|
||||
Supplier<Health> memoryCheck =
|
||||
() -> {
|
||||
MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean();
|
||||
MemoryUsage heapMemoryUsage = memoryMxBean.getHeapMemoryUsage();
|
||||
long maxMemory = heapMemoryUsage.getMax();
|
||||
long usedMemory = heapMemoryUsage.getUsed();
|
||||
|
||||
double memoryUsage = (double) usedMemory / maxMemory;
|
||||
String format = String.format("%.2f%% of %d max", memoryUsage * 100, maxMemory);
|
||||
|
||||
if (memoryUsage < memoryThreshold) {
|
||||
LOGGER.info("Memory usage is below threshold: {}", format);
|
||||
return Health.up().withDetail("memory usage", format).build();
|
||||
} else {
|
||||
return Health.down().withDetail("memory usage", format).build();
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
CompletableFuture<Health> future =
|
||||
asynchronousHealthChecker.performCheck(memoryCheck, timeoutInSeconds);
|
||||
return future.get();
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error("Health check interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
return Health.down().withDetail("error", "Health check interrupted").build();
|
||||
} catch (ExecutionException e) {
|
||||
LOGGER.error("Health check failed", e);
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
return Health.down().withDetail("error", cause.toString()).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the health status of the application by checking the memory usage.
|
||||
*
|
||||
* @return the health status of the application
|
||||
*/
|
||||
@Override
|
||||
public Health health() {
|
||||
return checkMemory();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.iluwatar.health.check;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.retry.backoff.FixedBackOffPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Configuration class for retry policies used in health check operations.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Configuration
|
||||
@Component
|
||||
public class RetryConfig {
|
||||
|
||||
/** The backoff period in milliseconds to wait between retry attempts. */
|
||||
@Value("${retry.backoff.period:2000}")
|
||||
private long backOffPeriod;
|
||||
|
||||
/** The maximum number of retry attempts for health check operations. */
|
||||
@Value("${retry.max.attempts:3}")
|
||||
private int maxAttempts;
|
||||
|
||||
/**
|
||||
* Creates a retry template with the configured backoff period and maximum number of attempts.
|
||||
*
|
||||
* @return a retry template
|
||||
*/
|
||||
@Bean
|
||||
public RetryTemplate retryTemplate() {
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
|
||||
fixedBackOffPolicy.setBackOffPeriod(backOffPeriod); // wait 2 seconds between retries
|
||||
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
|
||||
|
||||
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
|
||||
retryPolicy.setMaxAttempts(maxAttempts); // retry a max of 3 attempts
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
|
||||
return retryTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
server.port=6161
|
||||
management.endpoints.web.base-path=/actuator
|
||||
management.endpoint.health.probes.enabled=true
|
||||
management.health.livenessState.enabled=true
|
||||
management.health.readinessState.enabled=true
|
||||
management.endpoints.web.exposure.include=health,info
|
||||
|
||||
management.endpoint.health.show-details=always
|
||||
|
||||
# Enable health check logging
|
||||
logging.level.com.iluwatar.health.check=DEBUG
|
||||
|
||||
# H2 Database configuration
|
||||
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
|
||||
# H2 console available at /h2-console
|
||||
spring.h2.console.enabled=true
|
||||
spring.h2.console.path=/h2-console
|
||||
|
||||
# JPA Hibernate ddl auto (none, update, create, create-drop, validate)
|
||||
spring.jpa.hibernate.ddl-auto=create
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
spring.jpa.properties.hibernate.use_sql_comments=true
|
||||
|
||||
# Show SQL statements
|
||||
spring.jpa.show-sql=true
|
||||
|
||||
# Custom health check configuration
|
||||
health.check.timeout=10
|
||||
health.check.cache.evict.interval=60000
|
||||
|
||||
# CPU health check configuration
|
||||
cpu.system.load.threshold=95.0
|
||||
cpu.process.load.threshold=70.0
|
||||
cpu.load.average.threshold=10.0
|
||||
cpu.warning.message=CPU usage is high
|
||||
|
||||
# Retry configuration
|
||||
retry.backoff.period=2000
|
||||
retry.max.attempts=3
|
||||
|
||||
# Memory health check configuration
|
||||
memory.usage.threshold = 0.8
|
||||
@@ -0,0 +1,14 @@
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
import com.iluwatar.health.check.App;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Application test */
|
||||
class AppTest {
|
||||
|
||||
/** Entry point */
|
||||
@Test
|
||||
void shouldExecuteApplicationWithoutException() {
|
||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import com.iluwatar.health.check.AsynchronousHealthChecker;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
/**
|
||||
* Tests for {@link AsynchronousHealthChecker}.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
class AsynchronousHealthCheckerTest {
|
||||
|
||||
/** The {@link AsynchronousHealthChecker} instance to be tested. */
|
||||
private AsynchronousHealthChecker healthChecker;
|
||||
|
||||
private ListAppender<ILoggingEvent> listAppender;
|
||||
|
||||
@Mock private ScheduledExecutorService executorService;
|
||||
|
||||
public AsynchronousHealthCheckerTest() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the test environment before each test method.
|
||||
*
|
||||
* <p>Creates a new {@link AsynchronousHealthChecker} instance.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
healthChecker = new AsynchronousHealthChecker();
|
||||
// Replace the logger with the root logger of logback
|
||||
LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
|
||||
// Create and start a ListAppender
|
||||
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
listAppender = new ListAppender<>();
|
||||
listAppender.start();
|
||||
|
||||
// Add the appender to the root logger context
|
||||
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(listAppender);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears down the test environment after each test method.
|
||||
*
|
||||
* <p>Shuts down the {@link AsynchronousHealthChecker} instance to prevent resource leaks.
|
||||
*/
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
healthChecker.shutdown();
|
||||
((LoggerContext) LoggerFactory.getILoggerFactory()).reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the {@link performCheck()} method completes normally when the health supplier
|
||||
* returns a successful health check.
|
||||
*
|
||||
* <p>Given a health supplier that returns a healthy status, the test verifies that the {@link
|
||||
* performCheck()} method completes normally and returns the expected health object.
|
||||
*/
|
||||
@Test
|
||||
void whenPerformCheck_thenCompletesNormally() throws ExecutionException, InterruptedException {
|
||||
// Given
|
||||
Supplier<Health> healthSupplier = () -> Health.up().build();
|
||||
|
||||
// When
|
||||
CompletableFuture<Health> healthFuture = healthChecker.performCheck(healthSupplier, 3);
|
||||
|
||||
// Then
|
||||
Health health = healthFuture.get();
|
||||
assertEquals(Health.up().build(), health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the {@link performCheck()} method returns a healthy health status when the health
|
||||
* supplier returns a healthy status.
|
||||
*
|
||||
* <p>Given a health supplier that returns a healthy status, the test verifies that the {@link
|
||||
* performCheck()} method returns a health object with a status of UP.
|
||||
*/
|
||||
@Test
|
||||
void whenHealthCheckIsSuccessful_ReturnsHealthy()
|
||||
throws ExecutionException, InterruptedException {
|
||||
// Arrange
|
||||
Supplier<Health> healthSupplier = () -> Health.up().build();
|
||||
|
||||
// Act
|
||||
CompletableFuture<Health> healthFuture = healthChecker.performCheck(healthSupplier, 4);
|
||||
|
||||
// Assert
|
||||
assertEquals(Status.UP, healthFuture.get().getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the {@link performCheck()} method rejects new tasks after the {@link shutdown()}
|
||||
* method is called.
|
||||
*
|
||||
* <p>Given the {@link AsynchronousHealthChecker} instance is shut down, the test verifies that
|
||||
* the {@link performCheck()} method throws a {@link RejectedExecutionException} when attempting
|
||||
* to submit a new health check task.
|
||||
*/
|
||||
@Test
|
||||
void whenShutdown_thenRejectsNewTasks() {
|
||||
// Given
|
||||
healthChecker.shutdown();
|
||||
|
||||
// When/Then
|
||||
assertThrows(
|
||||
RejectedExecutionException.class,
|
||||
() -> healthChecker.performCheck(() -> Health.up().build(), 2),
|
||||
"Expected to throw RejectedExecutionException but did not");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the {@link performCheck()} method returns a healthy health status when the health
|
||||
* supplier returns a healthy status.
|
||||
*
|
||||
* <p>Given a health supplier that throws a RuntimeException, the test verifies that the {@link
|
||||
* performCheck()} method returns a health object with a status of DOWN and an error message
|
||||
* containing the exception message.
|
||||
*/
|
||||
@Test
|
||||
void whenHealthCheckThrowsException_thenReturnsDown() {
|
||||
// Arrange
|
||||
Supplier<Health> healthSupplier =
|
||||
() -> {
|
||||
throw new RuntimeException("Health check failed");
|
||||
};
|
||||
// Act
|
||||
CompletableFuture<Health> healthFuture = healthChecker.performCheck(healthSupplier, 10);
|
||||
// Assert
|
||||
Health health = healthFuture.join();
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
String errorMessage = health.getDetails().get("error").toString();
|
||||
assertTrue(errorMessage.contains("Health check failed"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if the log contains a specific message.
|
||||
*
|
||||
* @param action The action that triggers the log statement.
|
||||
* @return True if the log contains the message after the action is performed, false otherwise.
|
||||
*/
|
||||
private boolean doesLogContainMessage(Runnable action) {
|
||||
action.run();
|
||||
List<ILoggingEvent> events = listAppender.list;
|
||||
return events.stream()
|
||||
.anyMatch(event -> event.getMessage().contains("Health check executor did not terminate"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the {@link AsynchronousHealthChecker#shutdown()} method logs an error message when
|
||||
* the executor does not terminate after attempting to cancel tasks.
|
||||
*/
|
||||
@Test
|
||||
void whenShutdownExecutorDoesNotTerminateAfterCanceling_LogsErrorMessage() {
|
||||
// Given
|
||||
healthChecker.shutdown(); // To trigger the scenario
|
||||
|
||||
// When/Then
|
||||
boolean containsMessage = doesLogContainMessage(healthChecker::shutdown);
|
||||
if (!containsMessage) {
|
||||
List<ch.qos.logback.classic.spi.ILoggingEvent> events = listAppender.list;
|
||||
LOGGER.info("Logged events:");
|
||||
for (ch.qos.logback.classic.spi.ILoggingEvent event : events) {
|
||||
LOGGER.info(event.getMessage());
|
||||
}
|
||||
}
|
||||
assertTrue(containsMessage, "Expected log message not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that {@link AsynchronousHealthChecker#awaitTerminationWithTimeout} returns true even
|
||||
* if the executor service does not terminate completely within the specified timeout.
|
||||
*
|
||||
* @throws NoSuchMethodException if the private method cannot be accessed.
|
||||
* @throws InvocationTargetException if the private method throws an exception.
|
||||
* @throws IllegalAccessException if the private method is not accessible.
|
||||
* @throws InterruptedException if the thread is interrupted while waiting for the executor
|
||||
* service to terminate.
|
||||
*/
|
||||
@Test
|
||||
void awaitTerminationWithTimeout_IncompleteTermination_ReturnsTrue()
|
||||
throws NoSuchMethodException,
|
||||
InvocationTargetException,
|
||||
IllegalAccessException,
|
||||
InterruptedException {
|
||||
|
||||
// Mock executor service to return false (incomplete termination)
|
||||
when(executorService.awaitTermination(5, TimeUnit.SECONDS)).thenReturn(false);
|
||||
|
||||
// Use reflection to access the private method for code coverage.
|
||||
Method privateMethod =
|
||||
AsynchronousHealthChecker.class.getDeclaredMethod("awaitTerminationWithTimeout");
|
||||
privateMethod.setAccessible(true);
|
||||
|
||||
// When
|
||||
boolean result = (boolean) privateMethod.invoke(healthChecker);
|
||||
|
||||
// Then
|
||||
assertTrue(result, "Termination should be incomplete");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.iluwatar.health.check.CpuHealthIndicator;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
import java.lang.reflect.Field;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
/**
|
||||
* Test class for the {@link CpuHealthIndicator} class.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
class CpuHealthIndicatorTest {
|
||||
|
||||
/** The CPU health indicator to be tested. */
|
||||
private CpuHealthIndicator cpuHealthIndicator;
|
||||
|
||||
/** The mocked operating system MXBean used to simulate CPU health information. */
|
||||
private OperatingSystemMXBean mockOsBean;
|
||||
|
||||
/**
|
||||
* Sets up the test environment before each test method.
|
||||
*
|
||||
* <p>Mocks the {@link OperatingSystemMXBean} and sets it in the {@link CpuHealthIndicator}
|
||||
* instance.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Mock the com.sun.management.OperatingSystemMXBean
|
||||
mockOsBean = Mockito.mock(com.sun.management.OperatingSystemMXBean.class);
|
||||
cpuHealthIndicator = new CpuHealthIndicator();
|
||||
setOperatingSystemMXBean(cpuHealthIndicator, mockOsBean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflection method to set the private osBean in CpuHealthIndicator.
|
||||
*
|
||||
* @param indicator The CpuHealthIndicator instance to set the osBean for.
|
||||
* @param osBean The OperatingSystemMXBean to set.
|
||||
*/
|
||||
private void setOperatingSystemMXBean(
|
||||
CpuHealthIndicator indicator, OperatingSystemMXBean osBean) {
|
||||
try {
|
||||
Field osBeanField = CpuHealthIndicator.class.getDeclaredField("osBean");
|
||||
osBeanField.setAccessible(true);
|
||||
osBeanField.set(indicator, osBean);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the health status is DOWN when the system CPU load is high.
|
||||
*
|
||||
* <p>Sets the system CPU load to 90% and mocks the other getters to return appropriate values.
|
||||
* Executes the health check and asserts that the health status is DOWN and the error message
|
||||
* indicates high system CPU load.
|
||||
*/
|
||||
@Test
|
||||
void whenSystemCpuLoadIsHigh_thenHealthIsDown() {
|
||||
// Set thresholds for testing within the test method to avoid issues with Spring's @Value
|
||||
cpuHealthIndicator.setSystemCpuLoadThreshold(80.0);
|
||||
cpuHealthIndicator.setProcessCpuLoadThreshold(50.0);
|
||||
cpuHealthIndicator.setLoadAverageThreshold(0.75);
|
||||
|
||||
// Mock the getters to return your desired values
|
||||
when(mockOsBean.getCpuLoad()).thenReturn(0.9); // Simulate 90% system CPU load
|
||||
when(mockOsBean.getAvailableProcessors()).thenReturn(8);
|
||||
when(mockOsBean.getSystemLoadAverage()).thenReturn(9.0);
|
||||
|
||||
// Execute the health check
|
||||
Health health = cpuHealthIndicator.health();
|
||||
|
||||
// Assertions
|
||||
assertEquals(
|
||||
Status.DOWN,
|
||||
health.getStatus(),
|
||||
"Health status should be DOWN when system CPU load is high");
|
||||
assertEquals(
|
||||
"High system CPU load",
|
||||
health.getDetails().get("error"),
|
||||
"Error message should indicate high system CPU load");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the health status is DOWN when the process CPU load is high.
|
||||
*
|
||||
* <p>Sets the process CPU load to 80% and mocks the other getters to return appropriate values.
|
||||
* Executes the health check and asserts that the health status is DOWN and the error message
|
||||
* indicates high process CPU load.
|
||||
*/
|
||||
@Test
|
||||
void whenProcessCpuLoadIsHigh_thenHealthIsDown() {
|
||||
// Set thresholds for testing within the test method to avoid issues with Spring's @Value
|
||||
cpuHealthIndicator.setSystemCpuLoadThreshold(80.0);
|
||||
cpuHealthIndicator.setProcessCpuLoadThreshold(50.0);
|
||||
cpuHealthIndicator.setLoadAverageThreshold(0.75);
|
||||
|
||||
// Mock the getters to return your desired values
|
||||
when(mockOsBean.getCpuLoad()).thenReturn(0.5); // Simulate 50% system CPU load
|
||||
when(mockOsBean.getProcessCpuLoad()).thenReturn(0.8); // Simulate 80% process CPU load
|
||||
when(mockOsBean.getAvailableProcessors()).thenReturn(8);
|
||||
when(mockOsBean.getSystemLoadAverage()).thenReturn(5.0);
|
||||
|
||||
// Execute the health check
|
||||
Health health = cpuHealthIndicator.health();
|
||||
|
||||
// Assertions
|
||||
assertEquals(
|
||||
Status.DOWN,
|
||||
health.getStatus(),
|
||||
"Health status should be DOWN when process CPU load is high");
|
||||
assertEquals(
|
||||
"High process CPU load",
|
||||
health.getDetails().get("error"),
|
||||
"Error message should indicate high process CPU load");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.health.check.AsynchronousHealthChecker;
|
||||
import com.iluwatar.health.check.CustomHealthIndicator;
|
||||
import com.iluwatar.health.check.HealthCheckRepository;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Tests class< for {@link CustomHealthIndicator}. *
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
class CustomHealthIndicatorTest {
|
||||
|
||||
/** Mocked AsynchronousHealthChecker instance. */
|
||||
@Mock private AsynchronousHealthChecker healthChecker;
|
||||
|
||||
/** Mocked CacheManager instance. */
|
||||
@Mock private CacheManager cacheManager;
|
||||
|
||||
/** Mocked HealthCheckRepository instance. */
|
||||
@Mock private HealthCheckRepository healthCheckRepository;
|
||||
|
||||
/** Mocked Cache instance. */
|
||||
@Mock private Cache cache;
|
||||
|
||||
/** `CustomHealthIndicator` instance to be tested. */
|
||||
private CustomHealthIndicator customHealthIndicator;
|
||||
|
||||
/** Sets up the test environment. */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
when(cacheManager.getCache("health-check")).thenReturn(cache);
|
||||
customHealthIndicator =
|
||||
new CustomHealthIndicator(healthChecker, cacheManager, healthCheckRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the database is up.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the database is up, it returns a
|
||||
* Health object with Status.UP.
|
||||
*/
|
||||
@Test
|
||||
void whenDatabaseIsUp_thenHealthIsUp() {
|
||||
CompletableFuture<Health> future =
|
||||
CompletableFuture.completedFuture(Health.up().withDetail("database", "reachable").build());
|
||||
when(healthChecker.performCheck(any(), anyLong())).thenReturn(future);
|
||||
when(healthCheckRepository.checkHealth()).thenReturn(1);
|
||||
|
||||
Health health = customHealthIndicator.health();
|
||||
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the database is down.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the database is down, it returns a
|
||||
* Health object with Status.DOWN.
|
||||
*/
|
||||
@Test
|
||||
void whenDatabaseIsDown_thenHealthIsDown() {
|
||||
CompletableFuture<Health> future =
|
||||
CompletableFuture.completedFuture(
|
||||
Health.down().withDetail("database", "unreachable").build());
|
||||
when(healthChecker.performCheck(any(), anyLong())).thenReturn(future);
|
||||
when(healthCheckRepository.checkHealth()).thenReturn(null);
|
||||
|
||||
Health health = customHealthIndicator.health();
|
||||
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the health check times out.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the health check times out, it returns
|
||||
* a Health object with Status.DOWN.
|
||||
*/
|
||||
@Test
|
||||
void whenHealthCheckTimesOut_thenHealthIsDown() {
|
||||
CompletableFuture<Health> future = new CompletableFuture<>();
|
||||
when(healthChecker.performCheck(any(), anyLong())).thenReturn(future);
|
||||
|
||||
Health health = customHealthIndicator.health();
|
||||
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `evictHealthCache()` method.
|
||||
*
|
||||
* <p>Asserts that when the `evictHealthCache()` method is called, the health cache is cleared.
|
||||
*/
|
||||
@Test
|
||||
void whenEvictHealthCache_thenCacheIsCleared() {
|
||||
doNothing().when(cache).clear();
|
||||
|
||||
customHealthIndicator.evictHealthCache();
|
||||
|
||||
verify(cache, times(1)).clear();
|
||||
verify(cacheManager, times(1)).getCache("health-check");
|
||||
}
|
||||
|
||||
/** Configuration static class for the health check cache. */
|
||||
@Configuration
|
||||
static class CacheConfig {
|
||||
/**
|
||||
* Creates a concurrent map cache manager named "health-check".
|
||||
*
|
||||
* @return a new ConcurrentMapCacheManager instance
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("health-check");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.health.check.AsynchronousHealthChecker;
|
||||
import com.iluwatar.health.check.DatabaseTransactionHealthIndicator;
|
||||
import com.iluwatar.health.check.HealthCheckRepository;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link DatabaseTransactionHealthIndicator} class.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
class DatabaseTransactionHealthIndicatorTest {
|
||||
|
||||
/** Timeout value in seconds for the health check. */
|
||||
private final long timeoutInSeconds = 4;
|
||||
|
||||
/** Mocked HealthCheckRepository instance. */
|
||||
@Mock private HealthCheckRepository healthCheckRepository;
|
||||
|
||||
/** Mocked AsynchronousHealthChecker instance. */
|
||||
@Mock private AsynchronousHealthChecker asynchronousHealthChecker;
|
||||
|
||||
/** Mocked RetryTemplate instance. */
|
||||
@Mock private RetryTemplate retryTemplate;
|
||||
|
||||
/** `DatabaseTransactionHealthIndicator` instance to be tested. */
|
||||
private DatabaseTransactionHealthIndicator healthIndicator;
|
||||
|
||||
/** Performs initialization before each test method. */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
healthIndicator =
|
||||
new DatabaseTransactionHealthIndicator(
|
||||
healthCheckRepository, asynchronousHealthChecker, retryTemplate);
|
||||
healthIndicator.setTimeoutInSeconds(timeoutInSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the database transaction succeeds.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the database transaction succeeds, it
|
||||
* returns a Health object with Status.UP.
|
||||
*/
|
||||
@Test
|
||||
void whenDatabaseTransactionSucceeds_thenHealthIsUp() {
|
||||
CompletableFuture<Health> future = CompletableFuture.completedFuture(Health.up().build());
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), eq(timeoutInSeconds)))
|
||||
.thenReturn(future);
|
||||
|
||||
// Simulate the health check repository behavior
|
||||
doNothing().when(healthCheckRepository).performTestTransaction();
|
||||
|
||||
// Now call the actual method
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
// Check that the health status is UP
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the database transaction fails.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the database transaction fails, it
|
||||
* returns a Health object with Status.DOWN.
|
||||
*/
|
||||
@Test
|
||||
void whenDatabaseTransactionFails_thenHealthIsDown() {
|
||||
CompletableFuture<Health> future = new CompletableFuture<>();
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), eq(timeoutInSeconds)))
|
||||
.thenReturn(future);
|
||||
|
||||
// Simulate a database exception during the transaction
|
||||
doThrow(new RuntimeException("DB exception"))
|
||||
.when(healthCheckRepository)
|
||||
.performTestTransaction();
|
||||
|
||||
// Complete the future exceptionally to simulate a failure in the health check
|
||||
future.completeExceptionally(new RuntimeException("DB exception"));
|
||||
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
// Check that the health status is DOWN
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the health check times out.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the health check times out, it returns
|
||||
* a Health object with Status.DOWN.
|
||||
*/
|
||||
@Test
|
||||
void whenHealthCheckTimesOut_thenHealthIsDown() {
|
||||
CompletableFuture<Health> future = new CompletableFuture<>();
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), eq(timeoutInSeconds)))
|
||||
.thenReturn(future);
|
||||
|
||||
// Complete the future exceptionally to simulate a timeout
|
||||
future.completeExceptionally(new RuntimeException("Simulated timeout"));
|
||||
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
// Check that the health status is DOWN due to timeout
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.health.check.GarbageCollectionHealthIndicator;
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
/**
|
||||
* Test class for {@link GarbageCollectionHealthIndicator}.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
class GarbageCollectionHealthIndicatorTest {
|
||||
|
||||
/** Mocked garbage collector MXBean. */
|
||||
@Mock private GarbageCollectorMXBean garbageCollectorMXBean;
|
||||
|
||||
/** Mocked memory pool MXBean. */
|
||||
@Mock private MemoryPoolMXBean memoryPoolMXBean;
|
||||
|
||||
/** Garbage collection health indicator instance to be tested. */
|
||||
private GarbageCollectionHealthIndicator healthIndicator;
|
||||
|
||||
/** Set up the test environment before each test case. */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
healthIndicator =
|
||||
spy(
|
||||
new GarbageCollectionHealthIndicator() {
|
||||
@Override
|
||||
protected List<GarbageCollectorMXBean> getGarbageCollectorMxBeans() {
|
||||
return Collections.singletonList(garbageCollectorMXBean);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<MemoryPoolMXBean> getMemoryPoolMxBeans() {
|
||||
return Collections.singletonList(memoryPoolMXBean);
|
||||
}
|
||||
});
|
||||
healthIndicator.setMemoryUsageThreshold(0.8);
|
||||
}
|
||||
|
||||
/** Test case to verify that the health status is up when memory usage is low. */
|
||||
@Test
|
||||
void whenMemoryUsageIsLow_thenHealthIsUp() {
|
||||
when(garbageCollectorMXBean.getCollectionCount()).thenReturn(100L);
|
||||
when(garbageCollectorMXBean.getCollectionTime()).thenReturn(1000L);
|
||||
when(garbageCollectorMXBean.getMemoryPoolNames()).thenReturn(new String[] {"Eden Space"});
|
||||
|
||||
when(memoryPoolMXBean.getUsage()).thenReturn(new MemoryUsage(0, 100, 500, 1000));
|
||||
when(memoryPoolMXBean.getName()).thenReturn("Eden Space");
|
||||
|
||||
var health = healthIndicator.health();
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
}
|
||||
|
||||
/** Test case to verify that the health status contains a warning when memory usage is high. */
|
||||
@Test
|
||||
void whenMemoryUsageIsHigh_thenHealthContainsWarning() {
|
||||
// Arrange
|
||||
double threshold = 0.8; // 80% threshold for test
|
||||
healthIndicator.setMemoryUsageThreshold(threshold);
|
||||
|
||||
String poolName = "CodeCache";
|
||||
when(garbageCollectorMXBean.getName()).thenReturn("G1 Young Generation");
|
||||
when(garbageCollectorMXBean.getMemoryPoolNames()).thenReturn(new String[] {poolName});
|
||||
|
||||
long maxMemory = 1000L; // e.g., 1000 bytes
|
||||
long usedMemory = (long) (threshold * maxMemory) + 1; // e.g., 801 bytes to exceed 80% threshold
|
||||
when(memoryPoolMXBean.getUsage())
|
||||
.thenReturn(new MemoryUsage(0, usedMemory, usedMemory, maxMemory));
|
||||
when(memoryPoolMXBean.getName()).thenReturn(poolName);
|
||||
|
||||
// Act
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
// Assert
|
||||
Map<String, Object> gcDetails =
|
||||
(Map<String, Object>) health.getDetails().get("G1 Young Generation");
|
||||
assertNotNull(gcDetails, "Expected details for 'G1 Young Generation', but none were found.");
|
||||
|
||||
String memoryPoolsDetail = (String) gcDetails.get("memoryPools");
|
||||
assertNotNull(
|
||||
memoryPoolsDetail, "Expected memory pool details for 'CodeCache', but none were found.");
|
||||
|
||||
// Extracting the actual usage reported in the details for comparison
|
||||
String memoryUsageReported = memoryPoolsDetail.split(": ")[1].trim().replace("%", "");
|
||||
double memoryUsagePercentage = Double.parseDouble(memoryUsageReported);
|
||||
|
||||
assertTrue(
|
||||
memoryUsagePercentage > threshold,
|
||||
"Memory usage percentage should be above the threshold.");
|
||||
|
||||
String warning = (String) gcDetails.get("warning");
|
||||
assertNotNull(warning, "Expected a warning for high memory usage, but none was found.");
|
||||
|
||||
// Check that the warning message is as expected
|
||||
String expectedWarningRegex =
|
||||
String.format("Memory pool '%s' usage is high \\(\\d+\\.\\d+%%\\)", poolName);
|
||||
assertTrue(
|
||||
warning.matches(expectedWarningRegex),
|
||||
"Expected a high usage warning, but format is incorrect: " + warning);
|
||||
}
|
||||
|
||||
/** Test case to verify that the health status is up when there are no garbage collections. */
|
||||
@Test
|
||||
void whenNoGarbageCollections_thenHealthIsUp() {
|
||||
// Arrange: Mock the garbage collector to simulate no collections
|
||||
when(garbageCollectorMXBean.getCollectionCount()).thenReturn(0L);
|
||||
when(garbageCollectorMXBean.getCollectionTime()).thenReturn(0L);
|
||||
when(garbageCollectorMXBean.getName()).thenReturn("G1 Young Generation");
|
||||
when(garbageCollectorMXBean.getMemoryPoolNames()).thenReturn(new String[] {});
|
||||
|
||||
// Act: Perform the health check
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
// Assert: Ensure the health is up and there are no warnings
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
Map<String, Object> gcDetails =
|
||||
(Map<String, Object>) health.getDetails().get("G1 Young Generation");
|
||||
assertNotNull(gcDetails, "Expected details for 'G1 Young Generation', but none were found.");
|
||||
assertNull(
|
||||
gcDetails.get("warning"),
|
||||
"Expected no warning for 'G1 Young Generation' as there are no collections.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.iluwatar.health.check.HealthCheck;
|
||||
import com.iluwatar.health.check.HealthCheckRepository;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Tests class for {@link HealthCheckRepository}.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HealthCheckRepositoryTest {
|
||||
|
||||
/** Mocked EntityManager instance. */
|
||||
@Mock private EntityManager entityManager;
|
||||
|
||||
/** `HealthCheckRepository` instance to be tested. */
|
||||
@InjectMocks private HealthCheckRepository healthCheckRepository;
|
||||
|
||||
/**
|
||||
* Test case for the `performTestTransaction()` method.
|
||||
*
|
||||
* <p>Asserts that when the `performTestTransaction()` method is called, it successfully executes
|
||||
* a test transaction.
|
||||
*/
|
||||
@Test
|
||||
void whenCheckHealth_thenReturnsOne() {
|
||||
// Arrange
|
||||
Query mockedQuery = mock(Query.class);
|
||||
when(entityManager.createNativeQuery("SELECT 1")).thenReturn(mockedQuery);
|
||||
when(mockedQuery.getSingleResult()).thenReturn(1);
|
||||
|
||||
// Act
|
||||
Integer healthCheckResult = healthCheckRepository.checkHealth();
|
||||
|
||||
// Assert
|
||||
assertNotNull(healthCheckResult);
|
||||
assertEquals(1, healthCheckResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `performTestTransaction()` method.
|
||||
*
|
||||
* <p>Asserts that when the `performTestTransaction()` method is called, it successfully executes
|
||||
* a test transaction.
|
||||
*/
|
||||
@Test
|
||||
void whenPerformTestTransaction_thenSucceeds() {
|
||||
// Arrange
|
||||
HealthCheck healthCheck = new HealthCheck();
|
||||
healthCheck.setStatus("OK");
|
||||
|
||||
// Mocking the necessary EntityManager behaviors
|
||||
when(entityManager.find(eq(HealthCheck.class), any())).thenReturn(healthCheck);
|
||||
|
||||
// Act & Assert
|
||||
assertDoesNotThrow(() -> healthCheckRepository.performTestTransaction());
|
||||
|
||||
// Verify the interactions
|
||||
verify(entityManager).persist(any(HealthCheck.class));
|
||||
verify(entityManager).flush();
|
||||
verify(entityManager).remove(any(HealthCheck.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `checkHealth()` method when the database is down.
|
||||
*
|
||||
* <p>Asserts that when the `checkHealth()` method is called and the database is down, it throws a
|
||||
* RuntimeException.
|
||||
*/
|
||||
@Test
|
||||
void whenCheckHealth_andDatabaseIsDown_thenThrowsException() {
|
||||
// Arrange
|
||||
when(entityManager.createNativeQuery("SELECT 1")).thenThrow(RuntimeException.class);
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(RuntimeException.class, () -> healthCheckRepository.checkHealth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `performTestTransaction()` method when the persist operation fails.
|
||||
*
|
||||
* <p>Asserts that when the `performTestTransaction()` method is called and the persist operation
|
||||
* fails, it throws a RuntimeException.
|
||||
*/
|
||||
@Test
|
||||
void whenPerformTestTransaction_andPersistFails_thenThrowsException() {
|
||||
// Arrange
|
||||
doThrow(new RuntimeException()).when(entityManager).persist(any(HealthCheck.class));
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(RuntimeException.class, () -> healthCheckRepository.performTestTransaction());
|
||||
|
||||
// Verify that remove is not called if persist fails
|
||||
verify(entityManager, never()).remove(any(HealthCheck.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import com.iluwatar.health.check.App;
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.filter.log.LogDetail;
|
||||
import io.restassured.response.Response;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* Integration tests for the health endpoint.
|
||||
*
|
||||
* <p>* * Log statement for the test case response in case of "DOWN" status with high CPU load
|
||||
* during pipeline execution. * Note: During pipeline execution, if the health check shows "DOWN"
|
||||
* status with high CPU load, it is expected behavior. The service checks CPU usage, and if it's not
|
||||
* under 90%, it returns this error, example return value:
|
||||
* {"status":"DOWN","components":{"cpu":{"status":"DOWN","details":{"processCpuLoad":"100.00%", *
|
||||
* "availableProcessors":2,"systemCpuLoad":"100.00%","loadAverage":1.97,"timestamp":"2023-11-09T08:34:15.974557865Z",
|
||||
* * "error":"High system CPU load"}}} *
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootTest(
|
||||
classes = {App.class},
|
||||
webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
class HealthEndpointIntegrationTest {
|
||||
|
||||
/** Autowired TestRestTemplate instance for making HTTP requests. */
|
||||
@Autowired private TestRestTemplate restTemplate;
|
||||
|
||||
// Create a RequestSpecification that logs the request details
|
||||
private final RequestSpecification requestSpec =
|
||||
new RequestSpecBuilder().log(LogDetail.ALL).build();
|
||||
|
||||
private String getEndpointBasePath() {
|
||||
return restTemplate.getRootUri() + "/actuator/health";
|
||||
}
|
||||
|
||||
// Common method to log response details
|
||||
private void logResponseDetails(Response response) {
|
||||
LOGGER.info("Request URI: " + response.getDetailedCookies());
|
||||
LOGGER.info("Response Time: " + response.getTime() + "ms");
|
||||
LOGGER.info("Response Status: " + response.getStatusCode());
|
||||
LOGGER.info("Response: " + response.getBody().asString());
|
||||
}
|
||||
|
||||
/** Test that the health endpoint returns the UP status. */
|
||||
@Test
|
||||
void healthEndpointReturnsUpStatus() {
|
||||
Response response = given(requestSpec).get(getEndpointBasePath()).andReturn();
|
||||
logResponseDetails(response);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
|
||||
LOGGER.warn(
|
||||
"Health endpoint returned 503 Service Unavailable. This may be due to pipeline "
|
||||
+ "configuration. Please check the pipeline logs.");
|
||||
response.then().assertThat().statusCode(HttpStatus.SERVICE_UNAVAILABLE.value());
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK.value()
|
||||
|| !"UP".equals(response.path("status"))) {
|
||||
LOGGER.error("Health endpoint response: " + response.getBody().asString());
|
||||
LOGGER.error("Health endpoint status: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
response.then().assertThat().statusCode(HttpStatus.OK.value()).body("status", equalTo("UP"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the health endpoint returns complete details about the application's health. If the
|
||||
* status is 503, the test passes without further checks. If the status is 200, additional checks
|
||||
* are performed on various components. In case of a "DOWN" status, the test logs the entire
|
||||
* response for visibility.
|
||||
*/
|
||||
@Test
|
||||
void healthEndpointReturnsCompleteDetails() {
|
||||
// Make the HTTP request to the health endpoint
|
||||
Response response = given(requestSpec).get(getEndpointBasePath()).andReturn();
|
||||
|
||||
// Log the response details
|
||||
logResponseDetails(response);
|
||||
|
||||
// Check if the status is 503 (SERVICE_UNAVAILABLE)
|
||||
if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
|
||||
LOGGER.warn(
|
||||
"Health endpoint returned 503 Service Unavailable. This may be due to CI pipeline "
|
||||
+ "configuration. Please check the CI pipeline logs.");
|
||||
response
|
||||
.then()
|
||||
.assertThat()
|
||||
.statusCode(HttpStatus.SERVICE_UNAVAILABLE.value())
|
||||
.log()
|
||||
.all(); // Log the entire response for visibility
|
||||
return;
|
||||
}
|
||||
|
||||
// If status is 200, proceed with additional checks
|
||||
response
|
||||
.then()
|
||||
.assertThat()
|
||||
.statusCode(HttpStatus.OK.value()) // Check that the status is UP
|
||||
.body("status", equalTo("UP")) // Verify the status body is UP
|
||||
.body("components.cpu.status", equalTo("UP")) // Check CPU status
|
||||
.body("components.db.status", equalTo("UP")) // Check DB status
|
||||
.body("components.diskSpace.status", equalTo("UP")) // Check disk space status
|
||||
.body("components.ping.status", equalTo("UP")) // Check ping status
|
||||
.body("components.custom.status", equalTo("UP")); // Check custom component status
|
||||
|
||||
// Check for "DOWN" status and high CPU load
|
||||
if ("DOWN".equals(response.path("status"))) {
|
||||
LOGGER.error("Health endpoint response: " + response.getBody().asString());
|
||||
LOGGER.error("Health endpoint status: " + response.path("status"));
|
||||
LOGGER.error(
|
||||
"High CPU load detected: " + response.path("components.cpu.details.processCpuLoad"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the liveness endpoint returns the UP status.
|
||||
*
|
||||
* <p>The liveness endpoint is used to indicate whether the application is still running and
|
||||
* responsive.
|
||||
*/
|
||||
@Test
|
||||
void livenessEndpointShouldReturnUpStatus() {
|
||||
// Make the HTTP request to the liveness endpoint
|
||||
Response response = given(requestSpec).get(getEndpointBasePath() + "/liveness").andReturn();
|
||||
|
||||
// Log the response details
|
||||
logResponseDetails(response);
|
||||
|
||||
// Check if the status is 503 (SERVICE_UNAVAILABLE)
|
||||
if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
|
||||
LOGGER.warn(
|
||||
"Liveness endpoint returned 503 Service Unavailable. This may be due to CI pipeline "
|
||||
+ "configuration. Please check the CI pipeline logs.");
|
||||
// If status is 503, the test passes without further checks
|
||||
response
|
||||
.then()
|
||||
.assertThat()
|
||||
.statusCode(HttpStatus.SERVICE_UNAVAILABLE.value())
|
||||
.log()
|
||||
.all(); // Log the entire response for visibility
|
||||
return;
|
||||
}
|
||||
|
||||
// If status is 200, proceed with additional checks
|
||||
response.then().assertThat().statusCode(HttpStatus.OK.value()).body("status", equalTo("UP"));
|
||||
|
||||
// Check for "DOWN" status and high CPU load
|
||||
if ("DOWN".equals(response.path("status"))) {
|
||||
LOGGER.error("Liveness endpoint response: " + response.getBody().asString());
|
||||
LOGGER.error("Liveness endpoint status: " + response.path("status"));
|
||||
LOGGER.error(
|
||||
"High CPU load detected: " + response.path("components.cpu.details.processCpuLoad"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the custom health indicator returns the UP status and additional details.
|
||||
*
|
||||
* <p>The custom health indicator is used to provide more specific information about the health of
|
||||
* a particular component or aspect of the application.
|
||||
*/
|
||||
@Test
|
||||
void customHealthIndicatorShouldReturnUpStatusAndDetails() {
|
||||
// Make the HTTP request to the health endpoint
|
||||
Response response = given(requestSpec).get(getEndpointBasePath()).andReturn();
|
||||
|
||||
// Log the response details
|
||||
logResponseDetails(response);
|
||||
|
||||
// Check if the status is 503 (SERVICE_UNAVAILABLE)
|
||||
if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
|
||||
LOGGER.warn(
|
||||
"Custom health indicator returned 503 Service Unavailable. This may be due to CI pipeline "
|
||||
+ "configuration. Please check the CI pipeline logs.");
|
||||
// If status is 503, the test passes without further checks
|
||||
response
|
||||
.then()
|
||||
.assertThat()
|
||||
.statusCode(HttpStatus.SERVICE_UNAVAILABLE.value())
|
||||
.log()
|
||||
.all(); // Log the entire response for visibility
|
||||
return;
|
||||
}
|
||||
|
||||
// If status is 200, proceed with additional checks
|
||||
response
|
||||
.then()
|
||||
.assertThat()
|
||||
.statusCode(HttpStatus.OK.value()) // Check that the status is UP
|
||||
.body("components.custom.status", equalTo("UP")) // Verify the custom component status
|
||||
.body("components.custom.details.database", equalTo("reachable")); // Verify custom details
|
||||
|
||||
// Check for "DOWN" status and high CPU load
|
||||
if ("DOWN".equals(response.path("status"))) {
|
||||
LOGGER.error("Custom health indicator response: " + response.getBody().asString());
|
||||
LOGGER.error("Custom health indicator status: " + response.path("status"));
|
||||
LOGGER.error(
|
||||
"High CPU load detected: " + response.path("components.cpu.details.processCpuLoad"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.iluwatar.health.check.AsynchronousHealthChecker;
|
||||
import com.iluwatar.health.check.MemoryHealthIndicator;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MemoryHealthIndicator}.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MemoryHealthIndicatorTest {
|
||||
|
||||
/** Mocked AsynchronousHealthChecker instance. */
|
||||
@Mock private AsynchronousHealthChecker asynchronousHealthChecker;
|
||||
|
||||
/** `MemoryHealthIndicator` instance to be tested. */
|
||||
@InjectMocks private MemoryHealthIndicator memoryHealthIndicator;
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when memory usage is below the threshold.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and memory usage is below the threshold,
|
||||
* it returns a Health object with Status.UP.
|
||||
*/
|
||||
@Test
|
||||
void whenMemoryUsageIsBelowThreshold_thenHealthIsUp() {
|
||||
// Arrange
|
||||
CompletableFuture<Health> future =
|
||||
CompletableFuture.completedFuture(
|
||||
Health.up().withDetail("memory usage", "50% of max").build());
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), anyLong())).thenReturn(future);
|
||||
|
||||
// Act
|
||||
Health health = memoryHealthIndicator.health();
|
||||
|
||||
// Assert
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
assertEquals("50% of max", health.getDetails().get("memory usage"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when memory usage is above the threshold.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and memory usage is above the threshold,
|
||||
* it returns a Health object with Status.DOWN.
|
||||
*/
|
||||
@Test
|
||||
void whenMemoryUsageIsAboveThreshold_thenHealthIsDown() {
|
||||
// Arrange
|
||||
CompletableFuture<Health> future =
|
||||
CompletableFuture.completedFuture(
|
||||
Health.down().withDetail("memory usage", "95% of max").build());
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), anyLong())).thenReturn(future);
|
||||
|
||||
// Act
|
||||
Health health = memoryHealthIndicator.health();
|
||||
|
||||
// Assert
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
assertEquals("95% of max", health.getDetails().get("memory usage"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the health check is interrupted.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the health check is interrupted, it
|
||||
* returns a Health object with Status DOWN and an error detail indicating the interruption.
|
||||
*
|
||||
* @throws ExecutionException if the future fails to complete
|
||||
* @throws InterruptedException if the thread is interrupted while waiting for the future to
|
||||
* complete
|
||||
*/
|
||||
@Test
|
||||
void whenHealthCheckIsInterrupted_thenHealthIsDown()
|
||||
throws ExecutionException, InterruptedException {
|
||||
// Arrange
|
||||
CompletableFuture<Health> future = mock(CompletableFuture.class);
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), anyLong())).thenReturn(future);
|
||||
// Simulate InterruptedException when future.get() is called
|
||||
when(future.get()).thenThrow(new InterruptedException("Health check interrupted"));
|
||||
|
||||
// Act
|
||||
Health health = memoryHealthIndicator.health();
|
||||
|
||||
// Assert
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
String errorDetail = (String) health.getDetails().get("error");
|
||||
assertNotNull(errorDetail);
|
||||
assertTrue(errorDetail.contains("Health check interrupted"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the `health()` method when the health check execution fails.
|
||||
*
|
||||
* <p>Asserts that when the `health()` method is called and the health check execution fails, it
|
||||
* returns a Health object with Status DOWN and an error detail indicating the failure.
|
||||
*/
|
||||
@Test
|
||||
void whenHealthCheckExecutionFails_thenHealthIsDown() {
|
||||
// Arrange
|
||||
CompletableFuture<Health> future = new CompletableFuture<>();
|
||||
future.completeExceptionally(
|
||||
new ExecutionException(new RuntimeException("Service unavailable")));
|
||||
when(asynchronousHealthChecker.performCheck(any(Supplier.class), anyLong())).thenReturn(future);
|
||||
|
||||
// Act
|
||||
Health health = memoryHealthIndicator.health();
|
||||
|
||||
// Assert
|
||||
assertEquals(Status.DOWN, health.getStatus());
|
||||
assertTrue(health.getDetails().get("error").toString().contains("Service unavailable"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.iluwatar.health.check.RetryConfig;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link RetryConfig} class.
|
||||
*
|
||||
* @author ydoksanbir
|
||||
*/
|
||||
@SpringBootTest(classes = RetryConfig.class)
|
||||
class RetryConfigTest {
|
||||
|
||||
/** Injected RetryTemplate instance. */
|
||||
@Autowired private RetryTemplate retryTemplate;
|
||||
|
||||
/**
|
||||
* Tests that the retry template retries three times with a two-second delay.
|
||||
*
|
||||
* <p>Verifies that the retryable operation is executed three times before throwing an exception,
|
||||
* and that the total elapsed time for the retries is at least four seconds.
|
||||
*/
|
||||
@Test
|
||||
void shouldRetryThreeTimesWithTwoSecondDelay() {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
Runnable retryableOperation =
|
||||
() -> {
|
||||
attempts.incrementAndGet();
|
||||
throw new RuntimeException("Test exception for retry");
|
||||
};
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
retryTemplate.execute(
|
||||
context -> {
|
||||
retryableOperation.run();
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// Expected exception
|
||||
}
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
assertEquals(3, attempts.get(), "Should have retried three times");
|
||||
assertTrue(
|
||||
(endTime - startTime) >= 4000,
|
||||
"Should have waited at least 4 seconds in total for backoff");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user