From f2ea8494eaa24110f0c35fe4044c7f9f4871eab5 Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:51:38 +0530 Subject: [PATCH 1/4] Feat/health version (#3) * feat(health,version): add health and version endpoints * fix(jwt): fix the jwtvalidation issues * refactor(health): simplify MySQL health check and remove sensitive details * fix(health): harden advanced MySQL checks and throttle execution * fix(health): scope PROCESSLIST lock-wait check to application DB user * fix(health): cancel timed-out advanced MySQL checks to avoid orphaned tasks * fix(health): avoid sharing JDBC connections across threads in advanced MySQL checks * refactor(health): extract MySQL basic health query into helper method * fix(health): avoid blocking DB I/O under write lock and restore interrupt flag --- .github/workflows/swagger-json.yml | 107 ++++ README.md | 2 + pom.xml | 31 + .../controller/health/HealthController.java | 62 ++ .../controller/version/VersionController.java | 79 +++ .../fhir/service/health/HealthService.java | 573 ++++++++++++++++++ .../fhir/utils/JwtUserIdValidationFilter.java | 4 +- .../resources/application-swagger.properties | 133 ++++ 8 files changed, 990 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/swagger-json.yml create mode 100644 src/main/java/com/wipro/fhir/controller/health/HealthController.java create mode 100644 src/main/java/com/wipro/fhir/controller/version/VersionController.java create mode 100644 src/main/java/com/wipro/fhir/service/health/HealthService.java create mode 100644 src/main/resources/application-swagger.properties diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml new file mode 100644 index 0000000..d140252 --- /dev/null +++ b/.github/workflows/swagger-json.yml @@ -0,0 +1,107 @@ +name: Sync Swagger to AMRIT-Docs + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + swagger-sync: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout API repo + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: maven + + - name: Build API (skip tests) + run: mvn -B clean package -DskipTests + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Run API in swagger profile + run: | + nohup java -jar target/fhir-api-*.war \ + --spring.profiles.active=swagger \ + --server.port=9090 \ + > app.log 2>&1 & + echo $! > api_pid.txt + + - name: Wait for API & fetch Swagger + run: | + for i in {1..40}; do + CODE=$(curl --connect-timeout 2 --max-time 5 -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) + + if [ "$CODE" = "200" ]; then + jq . swagger_raw.json > fhir-api.json || { + echo "Swagger JSON invalid" + cat swagger_raw.json + exit 1 + } + + if [ "$(jq '.paths | length' fhir-api.json)" -eq 0 ]; then + echo "Swagger paths empty – failing" + exit 1 + fi + + echo "Swagger generated successfully" + exit 0 + fi + + echo "Waiting for API... ($i)" + sleep 4 + done + + echo "Swagger not generated" + cat app.log || true + exit 1 + + - name: Stop API + if: always() + run: | + # Graceful shutdown of the process group + sleep 5 + # Force kill the process group if still running + if [ -f api_pid.txt ]; then + PID=$(cat api_pid.txt) + kill -TERM -- -"$PID" 2>/dev/null || true + sleep 2 + kill -9 -- -"$PID" 2>/dev/null || true + fi + # Fallback: kill any remaining java process on port 9090 + fuser -k 9090/tcp 2>/dev/null || true + + - name: Checkout AMRIT-Docs + uses: actions/checkout@v4 + with: + repository: PSMRI/AMRIT-Docs + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + fetch-depth: 0 + + - name: Copy Swagger JSON + run: | + mkdir -p amrit-docs/docs/swagger + cp fhir-api.json amrit-docs/docs/swagger/fhir-api.json + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + branch: auto/swagger-update-${{ github.run_id }}-${{ github.run_attempt }} + base: main + commit-message: "chore(docs): auto-update FHIR-API swagger" + title: "chore(docs): auto-update FHIR-API swagger" + delete-branch: true + body: | + This PR automatically updates FHIR-API Swagger JSON + from the latest main branch build. diff --git a/README.md b/README.md index a6836c0..a52fdd3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # AMRIT - FHIR Service [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) ![branch parameter](https://github.com/PSMRI/FHIR-API/actions/workflows/sast.yml/badge.svg) +[![DeepWiki](https://img.shields.io/badge/DeepWiki-PSMRI%2FFHIR--API-blue)](https://deepwiki.com/PSMRI/FHIR-API) + FHIR (Fast Healthcare Interoperability Resources) standard defines how healthcare information can be exchanged between different computer systems regardless of how it is stored in those systems. FHIR provides a means for representing and sharing information among clinicians and organizations in a standard way regardless of the ways local EHRs represent or store the data. FHIR combines the best features of previous standards into a common specification, while being flexible enough to meet needs of a wide variety of use cases within the healthcare ecosystem. Resources are the basis for all exchangeable FHIR content. Each resource includes a standard definition and human-readable descriptions about how to use the resource. Each resource also has a set of common and resource-specific metadata (attributes) to allow its use clearly and unambiguously. FHIR Resources can store and/or exchange many types of clinical and administrative data. diff --git a/pom.xml b/pom.xml index 0a5c084..99c38e7 100644 --- a/pom.xml +++ b/pom.xml @@ -327,6 +327,11 @@ 0.12.6 runtime + + com.h2database + h2 + runtime + @@ -511,6 +516,32 @@ + + io.github.git-commit-id + git-commit-id-maven-plugin + 7.0.0 + + + get-the-git-infos + + revision + + initialize + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.branch$ + ^git.commit.id.abbrev$ + ^git.build.version$ + ^git.build.time$ + + false + false + + org.springframework.boot spring-boot-maven-plugin diff --git a/src/main/java/com/wipro/fhir/controller/health/HealthController.java b/src/main/java/com/wipro/fhir/controller/health/HealthController.java new file mode 100644 index 0000000..b0aaf77 --- /dev/null +++ b/src/main/java/com/wipro/fhir/controller/health/HealthController.java @@ -0,0 +1,62 @@ +package com.wipro.fhir.controller.health; + +import java.time.Instant; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.wipro.fhir.service.health.HealthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RestController +@RequestMapping("/health") +@Tag(name = "Health Check", description = "APIs for checking infrastructure health status") +public class HealthController { + + private static final Logger logger = LoggerFactory.getLogger(HealthController.class); + + private final HealthService healthService; + + public HealthController(HealthService healthService) { + this.healthService = healthService; + } + + @GetMapping + @Operation(summary = "Check infrastructure health", + description = "Returns the health status of MySQL, Redis, and other configured services") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Services are UP or DEGRADED (operational with warnings)"), + @ApiResponse(responseCode = "503", description = "One or more critical services are DOWN") + }) + public ResponseEntity> checkHealth() { + logger.info("Health check endpoint called"); + + try { + Map healthStatus = healthService.checkHealth(); + String overallStatus = (String) healthStatus.get("status"); + + // Return 503 only if DOWN; 200 for both UP and DEGRADED (DEGRADED = operational with warnings) + HttpStatus httpStatus = "DOWN".equals(overallStatus) ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK; + + logger.debug("Health check completed with status: {}", overallStatus); + return new ResponseEntity<>(healthStatus, httpStatus); + + } catch (Exception e) { + logger.error("Unexpected error during health check", e); + + Map errorResponse = Map.of( + "status", "DOWN", + "timestamp", Instant.now().toString() + ); + + return new ResponseEntity<>(errorResponse, HttpStatus.SERVICE_UNAVAILABLE); + } + } +} diff --git a/src/main/java/com/wipro/fhir/controller/version/VersionController.java b/src/main/java/com/wipro/fhir/controller/version/VersionController.java new file mode 100644 index 0000000..7a5af18 --- /dev/null +++ b/src/main/java/com/wipro/fhir/controller/version/VersionController.java @@ -0,0 +1,79 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.wipro.fhir.controller.version; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; + +@RestController +public class VersionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + private static final String UNKNOWN_VALUE = "unknown"; + + @Operation(summary = "Get version information") + @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> versionInformation() { + Map response = new LinkedHashMap<>(); + try { + logger.info("version Controller Start"); + Properties gitProperties = loadGitProperties(); + response.put("buildTimestamp", gitProperties.getProperty("git.build.time", UNKNOWN_VALUE)); + response.put("version", gitProperties.getProperty("git.build.version", UNKNOWN_VALUE)); + response.put("branch", gitProperties.getProperty("git.branch", UNKNOWN_VALUE)); + response.put("commitHash", gitProperties.getProperty("git.commit.id.abbrev", UNKNOWN_VALUE)); + } catch (Exception e) { + logger.error("Failed to load version information", e); + response.put("buildTimestamp", UNKNOWN_VALUE); + response.put("version", UNKNOWN_VALUE); + response.put("branch", UNKNOWN_VALUE); + response.put("commitHash", UNKNOWN_VALUE); + } + logger.info("version Controller End"); + return ResponseEntity.ok(response); + } + + private Properties loadGitProperties() throws IOException { + Properties properties = new Properties(); + try (InputStream input = getClass().getClassLoader() + .getResourceAsStream("git.properties")) { + if (input != null) { + properties.load(input); + } + } + return properties; + } +} diff --git a/src/main/java/com/wipro/fhir/service/health/HealthService.java b/src/main/java/com/wipro/fhir/service/health/HealthService.java new file mode 100644 index 0000000..4072bb5 --- /dev/null +++ b/src/main/java/com/wipro/fhir/service/health/HealthService.java @@ -0,0 +1,573 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +package com.wipro.fhir.service.health; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; +import jakarta.annotation.PreDestroy; +import javax.sql.DataSource; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariPoolMXBean; +import java.lang.management.ManagementFactory; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +public class HealthService { + + private static final Logger logger = LoggerFactory.getLogger(HealthService.class); + + // Status constants + private static final String STATUS_KEY = "status"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + + // Severity levels and keys + private static final String SEVERITY_KEY = "severity"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_CRITICAL = "CRITICAL"; + + // Response keys + private static final String ERROR_KEY = "error"; + private static final String MESSAGE_KEY = "message"; + private static final String RESPONSE_TIME_KEY = "responseTimeMs"; + + // Component names + private static final String MYSQL_COMPONENT = "MySQL"; + private static final String REDIS_COMPONENT = "Redis"; + + // Timeouts (in seconds) + private static final long MYSQL_TIMEOUT_SECONDS = 3; + private static final long REDIS_TIMEOUT_SECONDS = 3; + private static final long ADVANCED_CHECKS_TIMEOUT_MS = 500L; + private static final long ADVANCED_CHECKS_THROTTLE_SECONDS = 30; + private static final long RESPONSE_TIME_THRESHOLD_MS = 2000; + + // Diagnostic event codes for concise logging + private static final String DIAGNOSTIC_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String DIAGNOSTIC_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String DIAGNOSTIC_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + private static final String DIAGNOSTIC_LOG_TEMPLATE = "Diagnostic: {}"; + + private final DataSource dataSource; + private final RedisTemplate redisTemplate; + private final ExecutorService executorService; + private final ExecutorService advancedCheckExecutor; + private volatile long lastAdvancedCheckTime = 0; + private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; + private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); + + // Deadlock check resilience - disable after first permission error + private volatile boolean deadlockCheckDisabled = false; + + // Advanced checks always enabled + private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; + + public HealthService(DataSource dataSource, + @Autowired(required = false) RedisTemplate redisTemplate) { + this.dataSource = dataSource; + this.redisTemplate = redisTemplate; + this.executorService = Executors.newFixedThreadPool(6); + this.advancedCheckExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "health-advanced-check"); + t.setDaemon(true); + return t; + }); + } + + @PreDestroy + public void shutdown() { + if (executorService != null && !executorService.isShutdown()) { + try { + executorService.shutdown(); + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + logger.warn("ExecutorService did not terminate gracefully"); + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + logger.warn("ExecutorService shutdown interrupted", e); + } + } + if (advancedCheckExecutor != null && !advancedCheckExecutor.isShutdown()) { + advancedCheckExecutor.shutdownNow(); + } + } + + public Map checkHealth() { + Map response = new LinkedHashMap<>(); + response.put("timestamp", Instant.now().toString()); + + Map mysqlStatus = new ConcurrentHashMap<>(); + Map redisStatus = new ConcurrentHashMap<>(); + + if (!executorService.isShutdown()) { + performHealthChecks(mysqlStatus, redisStatus); + } + + ensurePopulated(mysqlStatus, MYSQL_COMPONENT); + ensurePopulated(redisStatus, REDIS_COMPONENT); + + Map> components = new LinkedHashMap<>(); + components.put("mysql", mysqlStatus); + components.put("redis", redisStatus); + + response.put("components", components); + response.put(STATUS_KEY, computeOverallStatus(components)); + + return response; + } + + private void performHealthChecks(Map mysqlStatus, Map redisStatus) { + Future mysqlFuture = executorService.submit( + () -> performHealthCheck(MYSQL_COMPONENT, mysqlStatus, this::checkMySQLHealthSync)); + Future redisFuture = executorService.submit( + () -> performHealthCheck(REDIS_COMPONENT, redisStatus, this::checkRedisHealthSync)); + + try { + awaitHealthChecks(mysqlFuture, redisFuture); + } catch (TimeoutException e) { + logger.warn("Health check aggregate timeout after {} seconds", getMaxTimeout()); + mysqlFuture.cancel(true); + redisFuture.cancel(true); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Health check was interrupted"); + mysqlFuture.cancel(true); + redisFuture.cancel(true); + } catch (Exception e) { + logger.warn("Health check execution error: {}", e.getMessage()); + mysqlFuture.cancel(true); + redisFuture.cancel(true); + } + } + + private void awaitHealthChecks(Future mysqlFuture, Future redisFuture) throws TimeoutException, InterruptedException, ExecutionException { + long maxTimeout = getMaxTimeout(); + long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); + + mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); + long remainingNs = deadlineNs - System.nanoTime(); + + if (remainingNs > 0) { + redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); + } else { + redisFuture.cancel(true); + } + } + + private long getMaxTimeout() { + return Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; + } + + + private void ensurePopulated(Map status, String componentName) { + if (!status.containsKey(STATUS_KEY)) { + status.put(STATUS_KEY, STATUS_DOWN); + status.put(SEVERITY_KEY, SEVERITY_CRITICAL); + status.put(ERROR_KEY, componentName + " health check did not complete in time"); + } + } + + private HealthCheckResult checkMySQLHealthSync() { + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement("SELECT 1 as health_check")) { + + stmt.setQueryTimeout((int) MYSQL_TIMEOUT_SECONDS); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + // Basic health check passed, now run advanced checks with throttling + boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); + return new HealthCheckResult(true, null, isDegraded); + } + } + + return new HealthCheckResult(false, "No result from health check query", false); + + } catch (Exception e) { + logger.warn("MySQL health check failed: {}", e.getMessage(), e); + return new HealthCheckResult(false, "MySQL connection failed", false); + } + } + + private HealthCheckResult checkRedisHealthSync() { + if (redisTemplate == null) { + return new HealthCheckResult(true, "Redis not configured — skipped", false); + } + + try { + String pong = redisTemplate.execute((org.springframework.data.redis.core.RedisCallback) (connection) -> connection.ping()); + + if ("PONG".equals(pong)) { + return new HealthCheckResult(true, null, false); + } + + return new HealthCheckResult(false, "Redis PING failed", false); + + } catch (Exception e) { + logger.warn("Redis health check failed: {}", e.getMessage(), e); + return new HealthCheckResult(false, "Redis connection failed", false); + } + } + + private Map performHealthCheck(String componentName, + Map status, + Supplier checker) { + long startTime = System.currentTimeMillis(); + + try { + HealthCheckResult result = checker.get(); + long responseTime = System.currentTimeMillis() - startTime; + + // Determine status: DOWN (unhealthy), DEGRADED (healthy but with issues), or UP + String componentStatus; + if (!result.isHealthy) { + componentStatus = STATUS_DOWN; + } else if (result.isDegraded) { + componentStatus = STATUS_DEGRADED; + } else { + componentStatus = STATUS_UP; + } + status.put(STATUS_KEY, componentStatus); + + // Set response time + status.put(RESPONSE_TIME_KEY, responseTime); + + // Determine severity based on health, response time, and degradation flags + String severity = determineSeverity(result.isHealthy, responseTime, result.isDegraded); + status.put(SEVERITY_KEY, severity); + + // Include message or error based on health status + if (result.error != null) { + // Use MESSAGE_KEY for informational messages when healthy + // Use ERROR_KEY for actual error messages when unhealthy + String fieldKey = result.isHealthy ? MESSAGE_KEY : ERROR_KEY; + status.put(fieldKey, result.error); + } + + return status; + + } catch (Exception e) { + long responseTime = System.currentTimeMillis() - startTime; + logger.error("{} health check failed with exception: {}", componentName, e.getMessage(), e); + + status.put(STATUS_KEY, STATUS_DOWN); + status.put(RESPONSE_TIME_KEY, responseTime); + status.put(SEVERITY_KEY, SEVERITY_CRITICAL); + status.put(ERROR_KEY, "Health check failed with an unexpected error"); + + return status; + } + } + + private String determineSeverity(boolean isHealthy, long responseTimeMs, boolean isDegraded) { + if (!isHealthy) { + return SEVERITY_CRITICAL; + } + + if (isDegraded) { + return SEVERITY_WARNING; + } + + if (responseTimeMs > RESPONSE_TIME_THRESHOLD_MS) { + return SEVERITY_WARNING; + } + + return SEVERITY_OK; + } + + private String computeOverallStatus(Map> components) { + boolean hasCritical = false; + boolean hasDegraded = false; + + for (Map componentStatus : components.values()) { + String status = (String) componentStatus.get(STATUS_KEY); + String severity = (String) componentStatus.get(SEVERITY_KEY); + + if (STATUS_DOWN.equals(status) || SEVERITY_CRITICAL.equals(severity)) { + hasCritical = true; + } + + if (STATUS_DEGRADED.equals(status)) { + hasDegraded = true; + } + + if (SEVERITY_WARNING.equals(severity)) { + hasDegraded = true; + } + } + + if (hasCritical) { + return STATUS_DOWN; + } + + if (hasDegraded) { + return STATUS_DEGRADED; + } + + return STATUS_UP; + } + + // Internal advanced health checks for MySQL - do not expose details in responses + private boolean performAdvancedMySQLChecksWithThrottle() { + if (!ADVANCED_HEALTH_CHECKS_ENABLED) { + return false; // Advanced checks disabled + } + + long currentTime = System.currentTimeMillis(); + + // Check throttle window - use read lock first for fast path + advancedCheckLock.readLock().lock(); + try { + if (cachedAdvancedCheckResult != null && + (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { + // Return cached result - within throttle window + return cachedAdvancedCheckResult.isDegraded; + } + } finally { + advancedCheckLock.readLock().unlock(); + } + + // Outside throttle window - acquire write lock and run checks + advancedCheckLock.writeLock().lock(); + try { + // Double-check after acquiring write lock + if (cachedAdvancedCheckResult != null && + (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { + return cachedAdvancedCheckResult.isDegraded; + } + + AdvancedCheckResult result; + Future future = + advancedCheckExecutor.submit(this::performAdvancedMySQLChecks); + try { + result = future.get(ADVANCED_CHECKS_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + logger.debug("Advanced MySQL checks timed out after {}ms", ADVANCED_CHECKS_TIMEOUT_MS); + future.cancel(true); + result = new AdvancedCheckResult(true); // treat timeout as degraded + } catch (ExecutionException ex) { + future.cancel(true); + // Check if the cause is an InterruptedException + if (ex.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + logger.debug("Advanced MySQL checks were interrupted"); + } else { + logger.debug("Advanced MySQL checks failed: {}", ex.getCause() != null ? ex.getCause().getMessage() : ex.getMessage()); + } + result = new AdvancedCheckResult(true); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.debug("Advanced MySQL checks interrupted"); + future.cancel(true); + result = new AdvancedCheckResult(true); + } catch (Exception ex) { + logger.debug("Advanced MySQL checks failed: {}", ex.getMessage()); + future.cancel(true); + result = new AdvancedCheckResult(true); + } + + // Cache the result + lastAdvancedCheckTime = currentTime; + cachedAdvancedCheckResult = result; + + return result.isDegraded; + } finally { + advancedCheckLock.writeLock().unlock(); + } + } + + private AdvancedCheckResult performAdvancedMySQLChecks() { + try (Connection connection = dataSource.getConnection()) { + return performAdvancedCheckLogic(connection); + } catch (Exception e) { + logger.debug("Advanced MySQL checks could not obtain connection: {}", e.getMessage()); + return new AdvancedCheckResult(true); + } + } + + private AdvancedCheckResult performAdvancedCheckLogic(Connection connection) { + try { + boolean hasIssues = false; + + if (hasLockWaits(connection)) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_LOCK_WAIT); + hasIssues = true; + } + + if (hasSlowQueries(connection)) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_SLOW_QUERIES); + hasIssues = true; + } + + if (hasConnectionPoolExhaustion()) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_POOL_EXHAUSTED); + hasIssues = true; + } + + return new AdvancedCheckResult(hasIssues); + } catch (Exception e) { + logger.debug("Advanced MySQL checks encountered exception, marking degraded"); + return new AdvancedCheckResult(true); + } + } + + private boolean hasLockWaits(Connection connection) { + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + + "WHERE (state = 'Waiting for table metadata lock' " + + " OR state = 'Waiting for row lock' " + + " OR state = 'Waiting for lock') " + + "AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { + stmt.setQueryTimeout(2); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + int lockCount = rs.getInt(1); + return lockCount > 0; + } + } + } catch (Exception e) { + logger.debug("Could not check for lock waits"); + } + return false; + } + + + private boolean hasSlowQueries(Connection connection) { + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + + "WHERE command != 'Sleep' AND time > ? AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { + stmt.setQueryTimeout(2); + stmt.setInt(1, 10); // Queries running longer than 10 seconds + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + int slowQueryCount = rs.getInt(1); + return slowQueryCount > 3; // Alert if more than 3 slow queries + } + } + } catch (Exception e) { + logger.debug("Could not check for slow queries"); + } + return false; + } + + private boolean hasConnectionPoolExhaustion() { + // Use HikariCP metrics if available + if (dataSource instanceof HikariDataSource hikariDataSource) { + try { + HikariPoolMXBean poolMXBean = hikariDataSource.getHikariPoolMXBean(); + + if (poolMXBean != null) { + int activeConnections = poolMXBean.getActiveConnections(); + int maxPoolSize = hikariDataSource.getMaximumPoolSize(); + + // Alert if > 80% of pool is exhausted + int threshold = (int) (maxPoolSize * 0.8); + return activeConnections > threshold; + } + } catch (Exception e) { + logger.debug("Could not retrieve HikariCP pool metrics"); + } + } + + // Fallback: try to get pool metrics via JMX if HikariCP is not directly available + return checkPoolMetricsViaJMX(); + } + + private boolean checkPoolMetricsViaJMX() { + try { + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (*)"); + var mBeans = mBeanServer.queryMBeans(objectName, null); + + for (var mBean : mBeans) { + if (evaluatePoolMetrics(mBeanServer, mBean.getObjectName())) { + return true; + } + } + } catch (Exception e) { + logger.debug("Could not access HikariCP pool metrics via JMX"); + } + + // No pool metrics available - disable this check + logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable"); + return false; + } + + private boolean evaluatePoolMetrics(MBeanServer mBeanServer, ObjectName objectName) { + try { + Integer activeConnections = (Integer) mBeanServer.getAttribute(objectName, "ActiveConnections"); + Integer maximumPoolSize = (Integer) mBeanServer.getAttribute(objectName, "MaximumPoolSize"); + + if (activeConnections != null && maximumPoolSize != null) { + int threshold = (int) (maximumPoolSize * 0.8); + return activeConnections > threshold; + } + } catch (Exception e) { + // Continue to next MBean + } + return false; + } + + private static class AdvancedCheckResult { + final boolean isDegraded; + + AdvancedCheckResult(boolean isDegraded) { + this.isDegraded = isDegraded; + } + } + + private static class HealthCheckResult { + final boolean isHealthy; + final String error; + final boolean isDegraded; + + HealthCheckResult(boolean isHealthy, String error, boolean isDegraded) { + this.isHealthy = isHealthy; + this.error = error; + this.isDegraded = isDegraded; + } + } +} diff --git a/src/main/java/com/wipro/fhir/utils/JwtUserIdValidationFilter.java b/src/main/java/com/wipro/fhir/utils/JwtUserIdValidationFilter.java index a924837..e688c58 100644 --- a/src/main/java/com/wipro/fhir/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/wipro/fhir/utils/JwtUserIdValidationFilter.java @@ -191,7 +191,9 @@ private boolean shouldSkipPath(String path, String contextPath) { || path.equalsIgnoreCase(contextPath + "/user/logOutUserFromConcurrentSession") || path.startsWith(contextPath + "/swagger-ui") || path.startsWith(contextPath + "/v3/api-docs") - || path.startsWith(contextPath + "/public"); + || path.startsWith(contextPath + "/public") + || path.equals(contextPath +"/version") + || path.equals(contextPath +"/health"); } private String getJwtTokenFromCookies(HttpServletRequest request) { diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties new file mode 100644 index 0000000..ce50e62 --- /dev/null +++ b/src/main/resources/application-swagger.properties @@ -0,0 +1,133 @@ +ndhmCreateHealthID=placeholder +# --- Swagger Profile Dummy Properties --- +# Database +spring.datasource.url=jdbc:h2:mem:swaggerdb +spring.datasource.username=SA +spring.datasource.password= +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=none +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +# Redis +spring.redis.host=localhost +spring.redis.port=6379 +spring.redis.password= + +# MongoDB (dummy connection string for CI/CD) +spring.data.mongodb.uri=mongodb://dummyuser:dummypassword@localhost:27017/test?authSource=admin + +# Quartz Scheduler +spring.quartz.job-store-type=memory + +# Dummy external API endpoints and keys +feedAuthUserName=placeholder +feedAuthPassword=placeholder +parentUrl=placeholder +atomFeedURLPatientDemographic=placeholder +abdmFacilityId=placeholder +generateOTPForCareContext=placeholder +validateOTPForCareContext=placeholder +addCareContext=placeholder +getAbdmFacilityServicies=placeholder +x-CM-ID=placeholder +abdmV3UserAuthenticate=placeholder +generateOTP_ForCard=placeholder +verifyOTP_ForCard=placeholder +verifyOTP_ForCard_Aadhaar=placeholder +generateHealthCard=placeholder +generateHealthIDCard=placeholder +ndhmGenerateOTPWithAadhaar=placeholder +abdmGenerateMobileOTP=placeholder +abdmConfirmAadhaarBio=placeholder +ndhmGenerateOTP=placeholder +ndhmVerifyOTP=placeholder +abdmcreateHealthIdWithPreVerified=placeholder +abdmVerifyOTP=placeholder +abdmCheckAndGenerateMobileOTP=placeholder +abdmVerifyMobileOTP=placeholder +abdmVerifyBio=placeholder + +# Other likely required placeholders (add more as needed) +server.port=8080 +logging.level.root=INFO +logging.level.org.springframework=INFO +management.endpoints.web.exposure.include=health,info +management.server.address=127.0.0.1 +management.server.port=8081 + +abdmVerifyBio=placeholder +abdmVerifyMobileOTP=placeholder +abdmCheckAndGenerateMobileOTP=placeholder +abdmVerifyOTP=placeholder +abdmcreateHealthIdWithPreVerified=placeholder +ndhmGenerateOTPWithAadhaar=placeholder +generateHealthIDCard=placeholder +generateHealthCard=placeholder +verifyOTP_ForCard_Aadhaar=placeholder +verifyOTP_ForCard=placeholder +generateOTP_ForCard=placeholder +abdmV3UserAuthenticate=placeholder +x-CM-ID=placeholder +spring.datasource.url=jdbc:h2:mem:swaggerdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=none + +# Disable Redis if not needed for docs (optional) +spring.redis.host=localhost +spring.redis.port=6379 + +cors.allowed-origins=http://localhost:8080 +logging.level.root=INFO +jwt.secret=sample-jwt-secret +eaushadhiStoreStockDetailsUrl=http://test-eaushadhi-details-url +eaushadhiStoreStockAckUrl=http://test-eaushadhi-ack-url +eAushadhiDummy=sample-eaushadhi +benSearchByBenIDURL=http://test-ben-search-url +userAuthURL=http://test-user-auth-url +fhirUserName=sample-fhir-username +fhirPassword=sample-fhir-password +abhaMode=sample-abha-mode +clientID=sample-client-id +clientSecret=sample-client-secret +ndhmuserAuthenticate=sample-ndhm-auth +generateABDM_NotifySMS=sample-abdm-notify-sms +atomsFeedStartPage=1 +feedAuthUserName=sample-feed-auth-username +feedAuthPassword=sample-feed-auth-password +parentUrl=http://test-parent-url-for-swagger +atomFeedURLPatientDemographic=http://test-patient-demographic-url-for-swagger +abdmFacilityId=sample-facility-id-for-swagger +generateOTPForCareContext=sample-generate-otp-for-care-context +validateOTPForCareContext=sample-validate-otp-for-care-context +addCareContext=sample-add-care-context +getAbdmFacilityServicies=sample-get-abdm-facility-services +atomFeedURLPatientEncounter=http://test-atom-feed-patient-encounter +atomFeedURLPatientClinical=http://test-atom-feed-patient-clinical +webLoginAbhaRequestOtp=sample-web-login-abha-request-otp +webLoginAbhaVerify=sample-web-login-abha-verify +verifyAbhaLogin=sample-verify-abha-login +abhaLoginRequestOtp=sample-abha-login-request-otp +abhaProfileLoginVerifyUser=sample-abha-profile-login-verify-user +webLoginPhrCard=sample-web-login-phr-card +requestOtpForEnrollment=sample-request-otp-for-enrollment +requestAuthByAbdm=sample-request-auth-by-abdm +abhaEnrollByAadhaar=sample-abha-enroll-by-aadhaar +printAbhaCard=sample-print-abha-card +getAuthCertPublicKey=sample-auth-cert-public-key +eAushadhiDispensePageSize=10 +patient-search-page-size=10 +nhm.agent.real.time.data.url=sample-nhm-agent-url +everwellCalendarDuration=10 +callRetryConfiguration=3 +sms-username=sample-sms-username +sms-password=sample-sms-password +sms-entityid=sample-sms-entityid +sms-consent-source-address=sample-sms-consent-source-address +send-message-url=sample-send-message-url +calibrationPageSize=10 +iemr.session.expiry.time.sec=3600 \ No newline at end of file From 7b0af9e89f07a8f753ff726736ae6fcab59d406e Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:53:51 +0530 Subject: [PATCH 2/4] Delete .github/workflows/swagger-json.yml --- .github/workflows/swagger-json.yml | 107 ----------------------------- 1 file changed, 107 deletions(-) delete mode 100644 .github/workflows/swagger-json.yml diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml deleted file mode 100644 index d140252..0000000 --- a/.github/workflows/swagger-json.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Sync Swagger to AMRIT-Docs - -on: - push: - branches: [ main ] - workflow_dispatch: - -jobs: - swagger-sync: - runs-on: ubuntu-latest - timeout-minutes: 20 - - steps: - - name: Checkout API repo - uses: actions/checkout@v4 - - - name: Set up Java 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - cache: maven - - - name: Build API (skip tests) - run: mvn -B clean package -DskipTests - - - name: Install jq - run: sudo apt-get update && sudo apt-get install -y jq - - - name: Run API in swagger profile - run: | - nohup java -jar target/fhir-api-*.war \ - --spring.profiles.active=swagger \ - --server.port=9090 \ - > app.log 2>&1 & - echo $! > api_pid.txt - - - name: Wait for API & fetch Swagger - run: | - for i in {1..40}; do - CODE=$(curl --connect-timeout 2 --max-time 5 -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) - - if [ "$CODE" = "200" ]; then - jq . swagger_raw.json > fhir-api.json || { - echo "Swagger JSON invalid" - cat swagger_raw.json - exit 1 - } - - if [ "$(jq '.paths | length' fhir-api.json)" -eq 0 ]; then - echo "Swagger paths empty – failing" - exit 1 - fi - - echo "Swagger generated successfully" - exit 0 - fi - - echo "Waiting for API... ($i)" - sleep 4 - done - - echo "Swagger not generated" - cat app.log || true - exit 1 - - - name: Stop API - if: always() - run: | - # Graceful shutdown of the process group - sleep 5 - # Force kill the process group if still running - if [ -f api_pid.txt ]; then - PID=$(cat api_pid.txt) - kill -TERM -- -"$PID" 2>/dev/null || true - sleep 2 - kill -9 -- -"$PID" 2>/dev/null || true - fi - # Fallback: kill any remaining java process on port 9090 - fuser -k 9090/tcp 2>/dev/null || true - - - name: Checkout AMRIT-Docs - uses: actions/checkout@v4 - with: - repository: PSMRI/AMRIT-Docs - token: ${{ secrets.DOCS_REPO_TOKEN }} - path: amrit-docs - fetch-depth: 0 - - - name: Copy Swagger JSON - run: | - mkdir -p amrit-docs/docs/swagger - cp fhir-api.json amrit-docs/docs/swagger/fhir-api.json - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ secrets.DOCS_REPO_TOKEN }} - path: amrit-docs - branch: auto/swagger-update-${{ github.run_id }}-${{ github.run_attempt }} - base: main - commit-message: "chore(docs): auto-update FHIR-API swagger" - title: "chore(docs): auto-update FHIR-API swagger" - delete-branch: true - body: | - This PR automatically updates FHIR-API Swagger JSON - from the latest main branch build. From f4f0c2f4de4b80773f89f157aeb829a1ab72751a Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:54:46 +0530 Subject: [PATCH 3/4] Delete src/main/resources/application-swagger.properties --- .../resources/application-swagger.properties | 133 ------------------ 1 file changed, 133 deletions(-) delete mode 100644 src/main/resources/application-swagger.properties diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties deleted file mode 100644 index ce50e62..0000000 --- a/src/main/resources/application-swagger.properties +++ /dev/null @@ -1,133 +0,0 @@ -ndhmCreateHealthID=placeholder -# --- Swagger Profile Dummy Properties --- -# Database -spring.datasource.url=jdbc:h2:mem:swaggerdb -spring.datasource.username=SA -spring.datasource.password= -spring.datasource.driver-class-name=org.h2.Driver -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=none -spring.jpa.show-sql=false -spring.jpa.properties.hibernate.format_sql=false - -# Redis -spring.redis.host=localhost -spring.redis.port=6379 -spring.redis.password= - -# MongoDB (dummy connection string for CI/CD) -spring.data.mongodb.uri=mongodb://dummyuser:dummypassword@localhost:27017/test?authSource=admin - -# Quartz Scheduler -spring.quartz.job-store-type=memory - -# Dummy external API endpoints and keys -feedAuthUserName=placeholder -feedAuthPassword=placeholder -parentUrl=placeholder -atomFeedURLPatientDemographic=placeholder -abdmFacilityId=placeholder -generateOTPForCareContext=placeholder -validateOTPForCareContext=placeholder -addCareContext=placeholder -getAbdmFacilityServicies=placeholder -x-CM-ID=placeholder -abdmV3UserAuthenticate=placeholder -generateOTP_ForCard=placeholder -verifyOTP_ForCard=placeholder -verifyOTP_ForCard_Aadhaar=placeholder -generateHealthCard=placeholder -generateHealthIDCard=placeholder -ndhmGenerateOTPWithAadhaar=placeholder -abdmGenerateMobileOTP=placeholder -abdmConfirmAadhaarBio=placeholder -ndhmGenerateOTP=placeholder -ndhmVerifyOTP=placeholder -abdmcreateHealthIdWithPreVerified=placeholder -abdmVerifyOTP=placeholder -abdmCheckAndGenerateMobileOTP=placeholder -abdmVerifyMobileOTP=placeholder -abdmVerifyBio=placeholder - -# Other likely required placeholders (add more as needed) -server.port=8080 -logging.level.root=INFO -logging.level.org.springframework=INFO -management.endpoints.web.exposure.include=health,info -management.server.address=127.0.0.1 -management.server.port=8081 - -abdmVerifyBio=placeholder -abdmVerifyMobileOTP=placeholder -abdmCheckAndGenerateMobileOTP=placeholder -abdmVerifyOTP=placeholder -abdmcreateHealthIdWithPreVerified=placeholder -ndhmGenerateOTPWithAadhaar=placeholder -generateHealthIDCard=placeholder -generateHealthCard=placeholder -verifyOTP_ForCard_Aadhaar=placeholder -verifyOTP_ForCard=placeholder -generateOTP_ForCard=placeholder -abdmV3UserAuthenticate=placeholder -x-CM-ID=placeholder -spring.datasource.url=jdbc:h2:mem:swaggerdb -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=none - -# Disable Redis if not needed for docs (optional) -spring.redis.host=localhost -spring.redis.port=6379 - -cors.allowed-origins=http://localhost:8080 -logging.level.root=INFO -jwt.secret=sample-jwt-secret -eaushadhiStoreStockDetailsUrl=http://test-eaushadhi-details-url -eaushadhiStoreStockAckUrl=http://test-eaushadhi-ack-url -eAushadhiDummy=sample-eaushadhi -benSearchByBenIDURL=http://test-ben-search-url -userAuthURL=http://test-user-auth-url -fhirUserName=sample-fhir-username -fhirPassword=sample-fhir-password -abhaMode=sample-abha-mode -clientID=sample-client-id -clientSecret=sample-client-secret -ndhmuserAuthenticate=sample-ndhm-auth -generateABDM_NotifySMS=sample-abdm-notify-sms -atomsFeedStartPage=1 -feedAuthUserName=sample-feed-auth-username -feedAuthPassword=sample-feed-auth-password -parentUrl=http://test-parent-url-for-swagger -atomFeedURLPatientDemographic=http://test-patient-demographic-url-for-swagger -abdmFacilityId=sample-facility-id-for-swagger -generateOTPForCareContext=sample-generate-otp-for-care-context -validateOTPForCareContext=sample-validate-otp-for-care-context -addCareContext=sample-add-care-context -getAbdmFacilityServicies=sample-get-abdm-facility-services -atomFeedURLPatientEncounter=http://test-atom-feed-patient-encounter -atomFeedURLPatientClinical=http://test-atom-feed-patient-clinical -webLoginAbhaRequestOtp=sample-web-login-abha-request-otp -webLoginAbhaVerify=sample-web-login-abha-verify -verifyAbhaLogin=sample-verify-abha-login -abhaLoginRequestOtp=sample-abha-login-request-otp -abhaProfileLoginVerifyUser=sample-abha-profile-login-verify-user -webLoginPhrCard=sample-web-login-phr-card -requestOtpForEnrollment=sample-request-otp-for-enrollment -requestAuthByAbdm=sample-request-auth-by-abdm -abhaEnrollByAadhaar=sample-abha-enroll-by-aadhaar -printAbhaCard=sample-print-abha-card -getAuthCertPublicKey=sample-auth-cert-public-key -eAushadhiDispensePageSize=10 -patient-search-page-size=10 -nhm.agent.real.time.data.url=sample-nhm-agent-url -everwellCalendarDuration=10 -callRetryConfiguration=3 -sms-username=sample-sms-username -sms-password=sample-sms-password -sms-entityid=sample-sms-entityid -sms-consent-source-address=sample-sms-consent-source-address -send-message-url=sample-send-message-url -calibrationPageSize=10 -iemr.session.expiry.time.sec=3600 \ No newline at end of file From e140c4656519dce7b87bc68d10a2501501f9dc6b Mon Sep 17 00:00:00 2001 From: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:55:51 +0530 Subject: [PATCH 4/4] Delete README.md --- README.md | 84 ------------------------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index a52fdd3..0000000 --- a/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# AMRIT - FHIR Service -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) ![branch parameter](https://github.com/PSMRI/FHIR-API/actions/workflows/sast.yml/badge.svg) -[![DeepWiki](https://img.shields.io/badge/DeepWiki-PSMRI%2FFHIR--API-blue)](https://deepwiki.com/PSMRI/FHIR-API) - - -FHIR (Fast Healthcare Interoperability Resources) standard defines how healthcare information can be exchanged between different computer systems regardless of how it is stored in those systems. FHIR provides a means for representing and sharing information among clinicians and organizations in a standard way regardless of the ways local EHRs represent or store the data. FHIR combines the best features of previous standards into a common specification, while being flexible enough to meet needs of a wide variety of use cases within the healthcare ecosystem. Resources are the basis for all exchangeable FHIR content. Each resource includes a standard definition and human-readable descriptions about how to use the resource. Each resource also has a set of common and resource-specific metadata (attributes) to allow its use clearly and unambiguously. FHIR Resources can store and/or exchange many types of clinical and administrative data. - -In AMRIT, currently we have developed 9 resources out of 27 resources. Contributors are working on developing rest of the 18 resources which will make AMRIT to be compliant with ABDM guidelines. FHIR R4 is the latest version which we are migrating from HL7 V2.0 current version of AMRIT application. - -### Key APIs in FHIR service -* Care Context Services -* e-Aushadhi -* ABHA Card Services -* OP Consultation Record Sharing -* Diagnostic Report Record Sharing -* Prescription Record Sharing -* Higher Health Facility - -## Environment and Setup -For setting up the development environment, please refer to the [Developer Guide](https://piramal-swasthya.gitbook.io/amrit/developer-guide/development-environment-setup) . - -## API Guide -Detailed information on API endpoints can be found in the [API Guide](https://piramal-swasthya.gitbook.io/amrit/architecture/api-guide). - -## Usage -All features have been exposed as REST endpoints. Refer to the SWAGGER API specification for details. - -## Setting Up Commit Hooks - -This project uses Git hooks to enforce consistent code quality and commit message standards. Even though this is a Java project, the hooks are powered by Node.js. Follow these steps to set up the hooks locally: - -### Prerequisites -- Node.js (v14 or later) -- npm (comes with Node.js) - -### Setup Steps - -1. **Install Node.js and npm** - - Download and install from [nodejs.org](https://nodejs.org/) - - Verify installation with: - ``` - node --version - npm --version - ``` - -2. **Install dependencies** - - From the project root directory, run: - ``` - npm ci - ``` - - This will install all required dependencies including Husky and commitlint - -3. **Verify hooks installation** - - The hooks should be automatically installed by Husky - - You can verify by checking if the `.husky` directory contains executable hooks - -### Commit Message Convention - -This project follows a specific commit message format: -- Format: `type(scope): subject` -- Example: `feat(login): add remember me functionality` - -Types include: -- `feat`: A new feature -- `fix`: A bug fix -- `docs`: Documentation changes -- `style`: Code style changes (formatting, etc.) -- `refactor`: Code changes that neither fix bugs nor add features -- `perf`: Performance improvements -- `test`: Adding or fixing tests -- `build`: Changes to build process or tools -- `ci`: Changes to CI configuration -- `chore`: Other changes (e.g., maintenance tasks, dependencies) - -Your commit messages will be automatically validated when you commit, ensuring project consistency. - -## Filing Issues - -If you encounter any issues, bugs, or have feature requests, please file them in the [main AMRIT repository](https://github.com/PSMRI/AMRIT/issues). Centralizing all feedback helps us streamline improvements and address concerns efficiently. - -## Join Our Community - -We’d love to have you join our community discussions and get real-time support! -Join our [Discord server](https://discord.gg/FVQWsf5ENS) to connect with contributors, ask questions, and stay updated.