diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index ae41b8e..ac16a9f 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -24,14 +24,18 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.util.HashMap; +import java.time.Instant; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; +import java.util.function.Supplier; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -41,6 +45,7 @@ public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); private static final String DB_HEALTH_CHECK_QUERY = "SELECT 1 as health_check"; + private static final String DB_VERSION_QUERY = "SELECT VERSION()"; @Autowired private DataSource dataSource; @@ -48,72 +53,230 @@ public class HealthService { @Autowired(required = false) private RedisTemplate redisTemplate; + @Value("${spring.datasource.url:unknown}") + private String dbUrl; + + @Value("${spring.redis.host:localhost}") + private String redisHost; + + @Value("${spring.redis.port:6379}") + private int redisPort; + public Map checkHealth() { - Map healthStatus = new HashMap<>(); + Map healthStatus = new LinkedHashMap<>(); + Map components = new LinkedHashMap<>(); boolean overallHealth = true; - // Check database connectivity (details logged internally, not exposed) - boolean dbHealthy = checkDatabaseHealthInternal(); - if (!dbHealthy) { + // Check MySQL connectivity + Map mysqlStatus = checkMySQLHealth(); + components.put("mysql", mysqlStatus); + if (!isHealthy(mysqlStatus)) { overallHealth = false; } - // Check Redis connectivity if configured (details logged internally) + // Check Redis connectivity if configured if (redisTemplate != null) { - boolean redisHealthy = checkRedisHealthInternal(); - if (!redisHealthy) { + Map redisStatus = checkRedisHealth(); + components.put("redis", redisStatus); + if (!isHealthy(redisStatus)) { overallHealth = false; } } healthStatus.put("status", overallHealth ? "UP" : "DOWN"); + healthStatus.put("timestamp", Instant.now().toString()); + healthStatus.put("components", components); logger.info("Health check completed - Overall status: {}", overallHealth ? "UP" : "DOWN"); return healthStatus; } - private boolean checkDatabaseHealthInternal() { - long startTime = System.currentTimeMillis(); - - try (Connection connection = dataSource.getConnection()) { - boolean isConnectionValid = connection.isValid(2); // 2 second timeout per best practices - - if (isConnectionValid) { - try (PreparedStatement stmt = connection.prepareStatement(DB_HEALTH_CHECK_QUERY)) { - stmt.setQueryTimeout(3); // 3 second query timeout - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next() && rs.getInt(1) == 1) { - long responseTime = System.currentTimeMillis() - startTime; - logger.debug("Database health check: UP ({}ms)", responseTime); - return true; + private Map checkMySQLHealth() { + Map details = new LinkedHashMap<>(); + details.put("type", "MySQL"); + details.put("host", extractHost(dbUrl)); + details.put("port", extractPort(dbUrl)); + details.put("database", extractDatabaseName(dbUrl)); + + return performHealthCheck("MySQL", details, () -> { + try { + try (Connection connection = dataSource.getConnection()) { + if (connection.isValid(2)) { + try (PreparedStatement stmt = connection.prepareStatement(DB_HEALTH_CHECK_QUERY)) { + stmt.setQueryTimeout(3); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next() && rs.getInt(1) == 1) { + String version = getMySQLVersion(connection); + return new HealthCheckResult(true, version, null); + } + } } } + return new HealthCheckResult(false, null, "Connection validation failed"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + private Map checkRedisHealth() { + Map details = new LinkedHashMap<>(); + details.put("type", "Redis"); + details.put("host", redisHost); + details.put("port", redisPort); + + return performHealthCheck("Redis", details, () -> { + String pong = redisTemplate.execute((RedisCallback) connection -> + connection.ping() + ); + if ("PONG".equals(pong)) { + String version = getRedisVersion(); + return new HealthCheckResult(true, version, null); + } + return new HealthCheckResult(false, null, "Ping returned unexpected response"); + }); + } + + + + /** + * Common health check execution pattern to reduce code duplication. + */ + private Map performHealthCheck(String componentName, + Map details, + Supplier checker) { + Map status = new LinkedHashMap<>(); + long startTime = System.currentTimeMillis(); + + try { + HealthCheckResult result = checker.get(); + long responseTime = System.currentTimeMillis() - startTime; + + if (result.isHealthy) { + logger.debug("{} health check: UP ({}ms)", componentName, responseTime); + status.put("status", "UP"); + details.put("responseTimeMs", responseTime); + if (result.version != null) { + details.put("version", result.version); } + } else { + logger.warn("{} health check: {}", componentName, result.error); + status.put("status", "DOWN"); + details.put("error", result.error); } - logger.warn("Database health check: Connection not valid"); - return false; + status.put("details", details); + return status; } catch (Exception e) { - logger.error("Database health check failed: {}", e.getMessage()); - return false; + logger.error("{} health check failed: {}", componentName, e.getMessage()); + status.put("status", "DOWN"); + details.put("error", e.getMessage()); + details.put("errorType", e.getClass().getSimpleName()); + status.put("details", details); + return status; } } - private boolean checkRedisHealthInternal() { - long startTime = System.currentTimeMillis(); - + private boolean isHealthy(Map componentStatus) { + return "UP".equals(componentStatus.get("status")); + } + + private String getMySQLVersion(Connection connection) { + try (PreparedStatement stmt = connection.prepareStatement(DB_VERSION_QUERY); + ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getString(1); + } + } catch (Exception e) { + logger.debug("Could not retrieve MySQL version: {}", e.getMessage()); + } + return null; + } + + private String getRedisVersion() { try { - String pong = redisTemplate.execute((RedisCallback) connection -> connection.ping()); - - if ("PONG".equals(pong)) { - long responseTime = System.currentTimeMillis() - startTime; - logger.debug("Redis health check: UP ({}ms)", responseTime); - return true; + Properties info = redisTemplate.execute((RedisCallback) connection -> + connection.serverCommands().info("server") + ); + if (info != null && info.containsKey("redis_version")) { + return info.getProperty("redis_version"); } - logger.warn("Redis health check: Ping returned unexpected response"); - return false; } catch (Exception e) { - logger.error("Redis health check failed: {}", e.getMessage()); - return false; + logger.debug("Could not retrieve Redis version: {}", e.getMessage()); + } + return null; + } + + + + private String extractHost(String jdbcUrl) { + if (jdbcUrl == null || "unknown".equals(jdbcUrl)) { + return "unknown"; + } + try { + String withoutPrefix = jdbcUrl.replaceFirst("jdbc:mysql://", ""); + int slashIndex = withoutPrefix.indexOf('/'); + String hostPort = slashIndex > 0 + ? withoutPrefix.substring(0, slashIndex) + : withoutPrefix; + int colonIndex = hostPort.indexOf(':'); + return colonIndex > 0 ? hostPort.substring(0, colonIndex) : hostPort; + } catch (Exception e) { + logger.debug("Could not extract host from URL: {}", e.getMessage()); + } + return "unknown"; + } + + private String extractPort(String jdbcUrl) { + if (jdbcUrl == null || "unknown".equals(jdbcUrl)) { + return "unknown"; + } + try { + String withoutPrefix = jdbcUrl.replaceFirst("jdbc:mysql://", ""); + int slashIndex = withoutPrefix.indexOf('/'); + String hostPort = slashIndex > 0 + ? withoutPrefix.substring(0, slashIndex) + : withoutPrefix; + int colonIndex = hostPort.indexOf(':'); + return colonIndex > 0 ? hostPort.substring(colonIndex + 1) : "3306"; + } catch (Exception e) { + logger.debug("Could not extract port from URL: {}", e.getMessage()); + } + return "3306"; + } + + private String extractDatabaseName(String jdbcUrl) { + if (jdbcUrl == null || "unknown".equals(jdbcUrl)) { + return "unknown"; + } + try { + int lastSlash = jdbcUrl.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash < jdbcUrl.length() - 1) { + String afterSlash = jdbcUrl.substring(lastSlash + 1); + int queryStart = afterSlash.indexOf('?'); + if (queryStart > 0) { + return afterSlash.substring(0, queryStart); + } + return afterSlash; + } + } catch (Exception e) { + logger.debug("Could not extract database name: {}", e.getMessage()); + } + return "unknown"; + } + + /** + * Internal class to hold health check results. + */ + private static class HealthCheckResult { + final boolean isHealthy; + final String version; + final String error; + + HealthCheckResult(boolean isHealthy, String version, String error) { + this.isHealthy = isHealthy; + this.version = version; + this.error = error; } } }