From a8b17d4a66a6175e07ad6ba254cb63fb0475a16c Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 2 Feb 2026 12:04:22 +0530 Subject: [PATCH 01/14] chore(swagger): automate swagger sync to amrit-docs --- .github/workflows/swagger-json.yml | 83 +++++++++++++++++++ pom.xml | 6 ++ .../iemr/common/config/PrimaryDBConfig.java | 3 +- .../iemr/common/config/SecondaryDBConfig.java | 5 +- .../ScheduleJobForNHMDashboardData.java | 1 + .../BeneficiaryRegistrationController.java | 2 + .../CustomerRelationshipSecondaryReports.java | 2 + .../callreport/CallReportSecondaryRepo.java | 2 + .../BenRelationshipTypeServiceImpl.java | 2 + .../BeneficiaryOccupationServiceImpl.java | 2 + .../RegisterBenificiaryServiceImpl.java | 3 +- .../SexualOrientationServiceImpl.java | 3 +- .../SecondaryReportService.java | 1 + .../SecondaryReportServiceImpl.java | 2 + .../com/iemr/common/utils/FilterConfig.java | 1 + .../common/utils/JwtAuthenticationUtil.java | 1 + .../data/report/SecondaryCallReport.java | 2 + .../resources/application-swagger.properties | 28 +++++++ src/main/resources/application.properties | 4 +- 19 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/swagger-json.yml 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 00000000..4a393358 --- /dev/null +++ b/.github/workflows/swagger-json.yml @@ -0,0 +1,83 @@ +name: Sync Swagger to AMRIT-Docs + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + swagger-sync: + runs-on: ubuntu-latest + + 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 clean package -DskipTests + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Run API in swagger profile + run: | + mvn spring-boot:run \ + -Dspring-boot.run.profiles=swagger \ + -Dspring-boot.run.arguments=--server.port=9090 \ + > app.log 2>&1 & + echo $! > api_pid.txt + + - name: Wait for API & fetch Swagger + run: | + for i in {1..30}; do + CODE=$(curl -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) + if [ "$CODE" = "200" ]; then + jq . swagger_raw.json > common-api.json + echo "Swagger generated successfully" + exit 0 + fi + echo "Waiting for API... ($i)" + sleep 5 + done + + echo "Swagger not generated" + cat app.log || true + exit 1 + + - name: Stop API + if: always() + run: | + if [ -f api_pid.txt ]; then + kill $(cat api_pid.txt) || true + fi + + - name: Checkout AMRIT-Docs + uses: actions/checkout@v4 + with: + repository: DurgaPrasad-54/AMRIT-Docs + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + + - name: Copy Swagger JSON + run: | + cp common-api.json amrit-docs/docs/swagger/common-api.json + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: amrit-docs + branch: auto/swagger-update + base: main + commit-message: Auto-update Common-API swagger + title: Auto-update Common-API swagger + body: | + This PR automatically updates the Common-API Swagger JSON + from the latest main branch build. diff --git a/pom.xml b/pom.xml index 11ad9f37..f818762c 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,12 @@ + + + com.h2database + h2 + runtime + org.springframework.boot diff --git a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java index 36463ab9..6101b135 100644 --- a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java @@ -47,7 +47,8 @@ @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory", basePackages = { "com.iemr.common.repository", - "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) + "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) +@org.springframework.context.annotation.Profile("!swagger") public class PrimaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/SecondaryDBConfig.java b/src/main/java/com/iemr/common/config/SecondaryDBConfig.java index 8a3928cb..3244612f 100644 --- a/src/main/java/com/iemr/common/config/SecondaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/SecondaryDBConfig.java @@ -43,10 +43,13 @@ import jakarta.persistence.EntityManagerFactory; +import org.springframework.context.annotation.Profile; + @Configuration @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "secondaryEntityManagerFactory", transactionManagerRef = "secondaryTransactionManager", basePackages = { - "com.iemr.common.secondary.repository.callreport" }) + "com.iemr.common.secondary.repository.callreport" }) +@Profile("!swagger") public class SecondaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java index fda36f0d..956566a9 100644 --- a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java +++ b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java @@ -31,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional; import com.iemr.common.service.nhm_dashboard.NHM_DashboardService; +import org.springframework.context.annotation.Profile; @Service @Transactional diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index 8f573d6d..3d9e204e 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -39,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.context.annotation.Profile; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -80,6 +81,7 @@ @RequestMapping({ "/beneficiary" }) @RestController +@Profile("!swagger") public class BeneficiaryRegistrationController { private InputMapper inputMapper = new InputMapper(); diff --git a/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java b/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java index 47d73255..88a8c863 100644 --- a/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java +++ b/src/main/java/com/iemr/common/controller/secondaryReport/CustomerRelationshipSecondaryReports.java @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.databind.ObjectMapper; @@ -46,6 +47,7 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; +@Profile("!swagger") @RequestMapping({ "/crmReports" }) @RestController public class CustomerRelationshipSecondaryReports { diff --git a/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java b/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java index 0a5dee7f..8e7e7ea8 100644 --- a/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java +++ b/src/main/java/com/iemr/common/secondary/repository/callreport/CallReportSecondaryRepo.java @@ -27,10 +27,12 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; import common.iemr.common.secondary.data.report.SecondaryCallReport; +@Profile("!swagger") @Repository public interface CallReportSecondaryRepo extends CrudRepository { @Query(value="call Pr_104QAReport(:startDateTime,:endDateTime,:receivedRoleName,:agentID,:providerServiceMapID)", nativeQuery=true) diff --git a/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java index 01a98eb2..31d9b227 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/BenRelationshipTypeServiceImpl.java @@ -31,7 +31,9 @@ import com.iemr.common.data.beneficiary.BenRelationshipType; import com.iemr.common.repository.beneficiary.BeneficiaryRelationshipTypeRepository; +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class BenRelationshipTypeServiceImpl implements BenRelationshipTypeService { private BeneficiaryRelationshipTypeRepository beneficiaryRelationshipTypeRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java index b272cb05..32bb9565 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/BeneficiaryOccupationServiceImpl.java @@ -31,8 +31,10 @@ import com.iemr.common.data.beneficiary.BeneficiaryOccupation; import com.iemr.common.repository.beneficiary.BeneficiaryOccupationRepository; +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class BeneficiaryOccupationServiceImpl implements BeneficiaryOccupationService { private BeneficiaryOccupationRepository beneficiaryOccupationRepository; diff --git a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java index 7d5f1de0..82acb58f 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/RegisterBenificiaryServiceImpl.java @@ -60,12 +60,13 @@ import com.iemr.common.utils.validator.Validator; import jakarta.servlet.http.HttpServletRequest; - +import org.springframework.context.annotation.Profile; /** * @author WA875423 * */ @Service +@Profile("!swagger") public class RegisterBenificiaryServiceImpl implements RegisterBenificiaryService { @Autowired diff --git a/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java index cd6d54c1..f910ced6 100644 --- a/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/beneficiary/SexualOrientationServiceImpl.java @@ -31,8 +31,9 @@ import com.iemr.common.data.beneficiary.SexualOrientation; import com.iemr.common.repository.userbeneficiarydata.SexualOrientationRepository; - +import org.springframework.context.annotation.Profile; @Service +@Profile("!swagger") public class SexualOrientationServiceImpl implements SexualOrientationService { private SexualOrientationRepository sexualOrientationRepository; diff --git a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java index a6ddfbfe..c6ebe089 100644 --- a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java +++ b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportService.java @@ -19,6 +19,7 @@ * 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.iemr.common.service.reportSecondary; import java.io.ByteArrayInputStream; diff --git a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java index 4107eb13..81a18611 100644 --- a/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java +++ b/src/main/java/com/iemr/common/service/reportSecondary/SecondaryReportServiceImpl.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import com.iemr.common.data.callhandling.CallType; @@ -53,6 +54,7 @@ import com.iemr.common.utils.mapper.InputMapper; +@Profile("!swagger") @Service public class SecondaryReportServiceImpl implements SecondaryReportService { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/utils/FilterConfig.java b/src/main/java/com/iemr/common/utils/FilterConfig.java index 42bd04ad..9144a296 100644 --- a/src/main/java/com/iemr/common/utils/FilterConfig.java +++ b/src/main/java/com/iemr/common/utils/FilterConfig.java @@ -33,6 +33,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; @Configuration +@org.springframework.context.annotation.Profile("!swagger") public class FilterConfig { private static final Logger log = LoggerFactory.getLogger(FilterConfig.class); diff --git a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java index 1e9f589d..381f64de 100644 --- a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; @Component +@org.springframework.context.annotation.Profile("!swagger") public class JwtAuthenticationUtil { @Autowired diff --git a/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java b/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java index 66f2777d..6c68f26d 100644 --- a/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java +++ b/src/main/java/common/iemr/common/secondary/data/report/SecondaryCallReport.java @@ -29,6 +29,7 @@ import com.iemr.common.utils.mapper.OutputMapper; import jakarta.persistence.Column; +import org.springframework.context.annotation.Profile; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -36,6 +37,7 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; +@Profile("!swagger") @Entity @Table(name = "fact_bencall", schema = "db_reporting") public class SecondaryCallReport implements Serializable diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties new file mode 100644 index 00000000..406d8401 --- /dev/null +++ b/src/main/resources/application-swagger.properties @@ -0,0 +1,28 @@ +cors.allowed-origins=* +# ---- Embedded DB for Swagger documentation generation +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 +spring.jpa.show-sql=false + +spring.sql.init.mode=never + +# Use placeholders for sensitive values +jwt.secret= +jwt.expiration=3600000 +sms-password= +sms-username= +start-grievancedatasync-scheduler=false +sms-consent-source-address= +send-message-url=http://localhost:8080/sms/sendMessage +secondary.datasource.username= +secondary.datasource.password= +secondary.datasource.url=jdbc:h2:mem:reportingdb +secondary.datasource.driver-class-name=org.h2.Driver + +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 18723465..b8cdef3f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -371,6 +371,4 @@ video-call-url = allowed.file.extensions=msg,pdf,png,jpeg,doc,docx,xlsx,xls,csv,txt ##sms details for beneficiary otp cosent -sms-template-name = otp_consent - - +sms-template-name = otp_consent \ No newline at end of file From 91485fb1681383a078ba712b383896b09ec3854c Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Tue, 3 Feb 2026 13:04:24 +0530 Subject: [PATCH 02/14] chore(swagger): automate swagger sync to amrit-docs --- .github/workflows/swagger-json.yml | 17 +++++++++++------ .../resources/application-swagger.properties | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index 4a393358..e7707698 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -37,11 +37,15 @@ jobs: - name: Wait for API & fetch Swagger run: | for i in {1..30}; do - CODE=$(curl -s -o swagger_raw.json -w "%{http_code}" http://localhost:9090/v3/api-docs || true) + 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 > common-api.json - echo "Swagger generated successfully" - exit 0 + if jq . swagger_raw.json > common-api.json; then + echo "Swagger generated successfully" + exit 0 + else + echo "Failed to parse swagger_raw.json with jq" + exit 1 + fi fi echo "Waiting for API... ($i)" sleep 5 @@ -61,12 +65,13 @@ jobs: - name: Checkout AMRIT-Docs uses: actions/checkout@v4 with: - repository: DurgaPrasad-54/AMRIT-Docs + repository: PSMRI/AMRIT-Docs token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs - name: Copy Swagger JSON run: | + mkdir -p amrit-docs/docs/swagger cp common-api.json amrit-docs/docs/swagger/common-api.json - name: Create Pull Request @@ -74,7 +79,7 @@ jobs: with: token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs - branch: auto/swagger-update + branch: auto/swagger-update-${{ github.run_id }} base: main commit-message: Auto-update Common-API swagger title: Auto-update Common-API swagger diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index 406d8401..f09e3c2b 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -1,4 +1,4 @@ -cors.allowed-origins=* +cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:9090,http://localhost:8080} # ---- Embedded DB for Swagger documentation generation spring.datasource.url=jdbc:h2:mem:swaggerdb spring.datasource.driver-class-name=org.h2.Driver @@ -12,7 +12,7 @@ spring.jpa.show-sql=false spring.sql.init.mode=never # Use placeholders for sensitive values -jwt.secret= +jwt.secret=JWT_SECRET jwt.expiration=3600000 sms-password= sms-username= From 40087b7437d7391fae5b00bbda57b118adb9f10c Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Wed, 4 Feb 2026 12:28:23 +0530 Subject: [PATCH 03/14] chore(swagger): automate swagger sync to amrit-docs --- .github/workflows/swagger-json.yml | 1 + src/main/java/com/iemr/common/config/PrimaryDBConfig.java | 3 ++- .../common/config/quartz/ScheduleJobForNHMDashboardData.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index e7707698..bac1707d 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -8,6 +8,7 @@ on: jobs: swagger-sync: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout API repo diff --git a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java index 6101b135..8a77a74a 100644 --- a/src/main/java/com/iemr/common/config/PrimaryDBConfig.java +++ b/src/main/java/com/iemr/common/config/PrimaryDBConfig.java @@ -39,6 +39,7 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.context.annotation.Profile; import com.iemr.common.utils.config.ConfigProperties; @@ -48,7 +49,7 @@ @EnableTransactionManagement @EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory", basePackages = { "com.iemr.common.repository", "com.iemr.common.repo", "com.iemr.common.notification.agent", "com.iemr.common.covidVaccination", "com.iemr.common.repository.everwell.*", "com.iemr.common.data.grievance", "com.iemr.common.repository.users" }) -@org.springframework.context.annotation.Profile("!swagger") +@Profile("!swagger") public class PrimaryDBConfig { Logger logger = LoggerFactory.getLogger(this.getClass().getName()); diff --git a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java index 956566a9..c9b29c62 100644 --- a/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java +++ b/src/main/java/com/iemr/common/config/quartz/ScheduleJobForNHMDashboardData.java @@ -35,6 +35,7 @@ @Service @Transactional +@Profile("!swagger") public class ScheduleJobForNHMDashboardData implements Job { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); From b10ba86d69f557494497e5c0cb67a4f3221c9e7f Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Wed, 11 Feb 2026 10:54:42 +0530 Subject: [PATCH 04/14] fix(swagger): update the workflow and fix the running issue --- .github/workflows/swagger-json.yml | 52 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index bac1707d..d1c2359a 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -22,8 +22,8 @@ jobs: cache: maven - name: Build API (skip tests) - run: mvn clean package -DskipTests - + run: mvn -B clean package -DskipTests + - name: Install jq run: sudo apt-get update && sudo apt-get install -y jq @@ -37,19 +37,27 @@ jobs: - name: Wait for API & fetch Swagger run: | - for i in {1..30}; do + 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 - if jq . swagger_raw.json > common-api.json; then - echo "Swagger generated successfully" - exit 0 - else - echo "Failed to parse swagger_raw.json with jq" + jq . swagger_raw.json > tm-api.json || { + echo "Swagger JSON invalid" + cat swagger_raw.json + exit 1 + } + + if [ "$(jq '.paths | length' tm-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 5 + sleep 4 done echo "Swagger not generated" @@ -59,9 +67,17 @@ jobs: - 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 - kill $(cat api_pid.txt) || true - fi + 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 @@ -69,21 +85,23 @@ jobs: 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 common-api.json amrit-docs/docs/swagger/common-api.json + cp tm-api.json amrit-docs/docs/swagger/tm-api.json - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs - branch: auto/swagger-update-${{ github.run_id }} + branch: auto/swagger-update-${{ github.run_id }}-${{ github.run_attempt }} base: main - commit-message: Auto-update Common-API swagger - title: Auto-update Common-API swagger + commit-message: "chore(docs): auto-update TM-API swagger" + title: "chore(docs): auto-update TM-API swagger" + delete-branch: true body: | - This PR automatically updates the Common-API Swagger JSON + This PR automatically updates TM-API Swagger JSON from the latest main branch build. From 54a79e1c57ba7471b70b528b8c245e31c94924b8 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Wed, 11 Feb 2026 20:46:05 +0530 Subject: [PATCH 05/14] fix(swagger): fix the swagger json workflow --- .github/workflows/swagger-json.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index d1c2359a..fa7aed85 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -41,13 +41,13 @@ jobs: 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 > tm-api.json || { + jq . swagger_raw.json > common-api.json || { echo "Swagger JSON invalid" cat swagger_raw.json exit 1 } - if [ "$(jq '.paths | length' tm-api.json)" -eq 0 ]; then + if [ "$(jq '.paths | length' common-api.json)" -eq 0 ]; then echo "Swagger paths empty – failing" exit 1 fi @@ -90,7 +90,7 @@ jobs: - name: Copy Swagger JSON run: | mkdir -p amrit-docs/docs/swagger - cp tm-api.json amrit-docs/docs/swagger/tm-api.json + cp common-api.json amrit-docs/docs/swagger/common-api.json - name: Create Pull Request uses: peter-evans/create-pull-request@v8 @@ -99,9 +99,9 @@ jobs: path: amrit-docs branch: auto/swagger-update-${{ github.run_id }}-${{ github.run_attempt }} base: main - commit-message: "chore(docs): auto-update TM-API swagger" - title: "chore(docs): auto-update TM-API swagger" + commit-message: "chore(docs): auto-update Common-API swagger" + title: "chore(docs): auto-update Common-API swagger" delete-branch: true body: | - This PR automatically updates TM-API Swagger JSON + This PR automatically updates Common-API Swagger JSON from the latest main branch build. From e4bc1ac3cdb3708333ddd1f185d93d78a33c6aec Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Wed, 11 Feb 2026 21:03:46 +0530 Subject: [PATCH 06/14] chore(swagger): add fixed branch name in workflow --- .github/workflows/swagger-json.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index fa7aed85..45009c8c 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -97,7 +97,7 @@ jobs: with: token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs - branch: auto/swagger-update-${{ github.run_id }}-${{ github.run_attempt }} + branch: auto/swagger-update base: main commit-message: "chore(docs): auto-update Common-API swagger" title: "chore(docs): auto-update Common-API swagger" From f8d94edc788ee7933fd75c46725b8e58a8280845 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Wed, 11 Feb 2026 21:17:58 +0530 Subject: [PATCH 07/14] chore(ci): prevent multiple swagger sync PRs by using fixed branch --- .github/workflows/swagger-json.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/swagger-json.yml b/.github/workflows/swagger-json.yml index 45009c8c..413643f7 100644 --- a/.github/workflows/swagger-json.yml +++ b/.github/workflows/swagger-json.yml @@ -92,12 +92,14 @@ jobs: mkdir -p amrit-docs/docs/swagger cp common-api.json amrit-docs/docs/swagger/common-api.json + # Use a fixed branch name for PRs to avoid accumulating stale PRs. + # This ensures only one open PR is updated per run; delete-branch: true cleans up after merge. - name: Create Pull Request uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.DOCS_REPO_TOKEN }} path: amrit-docs - branch: auto/swagger-update + branch: auto/swagger-update-common-api base: main commit-message: "chore(docs): auto-update Common-API swagger" title: "chore(docs): auto-update Common-API swagger" From 452be069020af2d97334e4c7897e898216e29b9a Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Thu, 12 Feb 2026 11:43:16 +0530 Subject: [PATCH 08/14] chore(swagger): add Dev/UAT/Demo servers to OpenAPI config --- .../com/iemr/common/config/SwaggerConfig.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/common/config/SwaggerConfig.java b/src/main/java/com/iemr/common/config/SwaggerConfig.java index 793f3a25..a986ecf5 100644 --- a/src/main/java/com/iemr/common/config/SwaggerConfig.java +++ b/src/main/java/com/iemr/common/config/SwaggerConfig.java @@ -12,13 +12,18 @@ @Configuration public class SwaggerConfig { - @Bean + @Bean public OpenAPI customOpenAPI() { - return new OpenAPI().info(new - Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) - .addSecurityItem(new SecurityRequirement().addList("my security")) - .components(new Components().addSecuritySchemes("my security", - new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))); + return new OpenAPI() + .info(new Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) + .addSecurityItem(new SecurityRequirement().addList("my security")) + .components(new Components().addSecuritySchemes("my security", + new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))) + .servers(java.util.Arrays.asList( + new io.swagger.v3.oas.models.servers.Server().url(System.getenv().getOrDefault("API_DEV_URL", "https://amritwprdev.piramalswasthya.org")).description("Dev"), + new io.swagger.v3.oas.models.servers.Server().url(System.getenv().getOrDefault("API_UAT_URL", "https://uatamrit.piramalswasthya.org")).description("UAT"), + new io.swagger.v3.oas.models.servers.Server().url(System.getenv().getOrDefault("API_DEMO_URL", "https://amritdemo.piramalswasthya.org")).description("Demo") + )); } } From 4ef377f4197eb7f50533dc500680d027881ff068 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Thu, 12 Feb 2026 12:17:12 +0530 Subject: [PATCH 09/14] chore(swagger): avoid default server URLs --- .../com/iemr/common/config/SwaggerConfig.java | 15 +++++++++++---- src/main/resources/application-swagger.properties | 6 +++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/common/config/SwaggerConfig.java b/src/main/java/com/iemr/common/config/SwaggerConfig.java index a986ecf5..7ffc72be 100644 --- a/src/main/java/com/iemr/common/config/SwaggerConfig.java +++ b/src/main/java/com/iemr/common/config/SwaggerConfig.java @@ -2,6 +2,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -11,18 +13,23 @@ @Configuration public class SwaggerConfig { - + @Autowired + private Environment env; + @Bean public OpenAPI customOpenAPI() { + String devUrl = env.getProperty("API_DEV_URL"); + String uatUrl = env.getProperty("API_UAT_URL"); + String demoUrl = env.getProperty("API_DEMO_URL"); return new OpenAPI() .info(new Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) .addSecurityItem(new SecurityRequirement().addList("my security")) .components(new Components().addSecuritySchemes("my security", new SecurityScheme().name("my security").type(SecurityScheme.Type.HTTP).scheme("bearer"))) .servers(java.util.Arrays.asList( - new io.swagger.v3.oas.models.servers.Server().url(System.getenv().getOrDefault("API_DEV_URL", "https://amritwprdev.piramalswasthya.org")).description("Dev"), - new io.swagger.v3.oas.models.servers.Server().url(System.getenv().getOrDefault("API_UAT_URL", "https://uatamrit.piramalswasthya.org")).description("UAT"), - new io.swagger.v3.oas.models.servers.Server().url(System.getenv().getOrDefault("API_DEMO_URL", "https://amritdemo.piramalswasthya.org")).description("Demo") + new io.swagger.v3.oas.models.servers.Server().url(devUrl).description("Dev"), + new io.swagger.v3.oas.models.servers.Server().url(uatUrl).description("UAT"), + new io.swagger.v3.oas.models.servers.Server().url(demoUrl).description("Demo") )); } diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index f09e3c2b..a24bbfad 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -25,4 +25,8 @@ secondary.datasource.url=jdbc:h2:mem:reportingdb secondary.datasource.driver-class-name=org.h2.Driver springdoc.api-docs.enabled=true -springdoc.swagger-ui.enabled=true \ No newline at end of file +springdoc.swagger-ui.enabled=true + +API_DEV_URL = https://amritwprdev.piramalswasthya.org +API_UAT_URL = https://uatamrit.piramalswasthya.org +API_DEMO_URL = https://amritdemo.piramalswasthya.org \ No newline at end of file From e9eb39e111431bc7bf8555295e9a78a2d1b6fce1 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Thu, 12 Feb 2026 14:08:05 +0530 Subject: [PATCH 10/14] chore(swagger): remove field injection and inject URLs into OpenAPI bean --- .../java/com/iemr/common/config/SwaggerConfig.java | 12 +++++------- src/main/resources/application-swagger.properties | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/iemr/common/config/SwaggerConfig.java b/src/main/java/com/iemr/common/config/SwaggerConfig.java index 7ffc72be..04bcec21 100644 --- a/src/main/java/com/iemr/common/config/SwaggerConfig.java +++ b/src/main/java/com/iemr/common/config/SwaggerConfig.java @@ -2,7 +2,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import io.swagger.v3.oas.models.Components; @@ -13,14 +12,13 @@ @Configuration public class SwaggerConfig { - @Autowired - private Environment env; + private static final String DEFAULT_SERVER_URL = "http://localhost:9090"; @Bean - public OpenAPI customOpenAPI() { - String devUrl = env.getProperty("API_DEV_URL"); - String uatUrl = env.getProperty("API_UAT_URL"); - String demoUrl = env.getProperty("API_DEMO_URL"); + public OpenAPI customOpenAPI(Environment env) { + String devUrl = env.getProperty("api.dev.url", DEFAULT_SERVER_URL); + String uatUrl = env.getProperty("api.uat.url", DEFAULT_SERVER_URL); + String demoUrl = env.getProperty("api.demo.url", DEFAULT_SERVER_URL); return new OpenAPI() .info(new Info().title("Common API").version("version").description("A microservice for the creation and management of beneficiaries.")) .addSecurityItem(new SecurityRequirement().addList("my security")) diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index a24bbfad..f73e6fd2 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -27,6 +27,6 @@ secondary.datasource.driver-class-name=org.h2.Driver springdoc.api-docs.enabled=true springdoc.swagger-ui.enabled=true -API_DEV_URL = https://amritwprdev.piramalswasthya.org -API_UAT_URL = https://uatamrit.piramalswasthya.org -API_DEMO_URL = https://amritdemo.piramalswasthya.org \ No newline at end of file +api.dev.url=${API_DEV_URL:https://amritwprdev.piramalswasthya.org} +api.uat.url=${API_UAT_URL:https://uatamrit.piramalswasthya.org} +api.demo.url=${API_DEMO_URL:https://amritdemo.piramalswasthya.org} \ No newline at end of file From 5e9de591c999e3d82f4171fbf354f0ddb9ad26d2 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 23 Feb 2026 10:46:06 +0530 Subject: [PATCH 11/14] feat(health,version): update version and health endpoints and add advance check for database --- pom.xml | 26 ++ .../controller/version/VersionController.java | 57 +-- .../common/service/health/HealthService.java | 344 +++++++++++++++--- .../utils/JwtUserIdValidationFilter.java | 4 +- 4 files changed, 367 insertions(+), 64 deletions(-) diff --git a/pom.xml b/pom.xml index f818762c..f0badf34 100644 --- a/pom.xml +++ b/pom.xml @@ -526,6 +526,32 @@ ${artifactId}-${version} + + io.github.git-commit-id + git-commit-id-maven-plugin + 9.0.2 + + + 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.apache.maven.plugins maven-jar-plugin diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index 10645866..a6e6d828 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -22,8 +22,8 @@ /** * REST controller exposing application version and build metadata. *

- * Provides the /version endpoint which returns the - * Git commit hash and build timestamp in a standardized JSON format. + * Provides the /version endpoint which returns Git metadata + * in a standardized JSON format consistent across all AMRIT APIs. *

* * @author Vaishnav Bhosale @@ -31,12 +31,16 @@ package com.iemr.common.controller.version; import java.io.InputStream; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; +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; @@ -46,28 +50,39 @@ public class VersionController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + private static final String UNKNOWN_VALUE = "unknown"; - @Operation(summary = "Get version") - @RequestMapping(value = "/version", method = { RequestMethod.GET }) - public VersionInfo versionInformation() { + @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 is = getClass() - .getClassLoader() + try (InputStream input = getClass().getClassLoader() .getResourceAsStream("git.properties")) { - - if (is != null) { - properties.load(is); + if (input != null) { + properties.load(input); } - - } catch (Exception e) { - logger.error("Error reading git.properties", e); } - - return new VersionInfo( - properties.getProperty("git.commit.id.abbrev", "unknown"), - properties.getProperty("git.build.time", "unknown") - ); + return properties; } } diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index 1e312a09..d117337e 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -23,92 +23,352 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.ObjectProvider; // <--- VITAL IMPORT +import org.springframework.beans.factory.ObjectProvider; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Service; +import javax.annotation.PreDestroy; import javax.sql.DataSource; import java.sql.Connection; -import java.util.HashMap; +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.Instant; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; -/** - * Health check service for Common-API. - * Verifies application liveness and dependency health (DB, Redis). - * - * @author vaishnavbhosale - */ @Service public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); - private static final String COMPONENT_DATABASE = "database"; - private static final String COMPONENT_REDIS = "redis"; - + private static final String LOG_EVENT_STUCK_PROCESS = "MYSQL_STUCK_PROCESS"; + private static final String LOG_EVENT_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String LOG_EVENT_DEADLOCK = "MYSQL_DEADLOCK"; + private static final String LOG_EVENT_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String LOG_EVENT_CONN_USAGE = "MYSQL_CONNECTION_USAGE"; + private static final String LOG_EVENT_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW + private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING + private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds + private static final int CONNECTION_USAGE_WARNING = 80; // > 80% → WARNING + private static final int CONNECTION_USAGE_CRITICAL= 95; // > 95% → CRITICAL + private static final long DIAGNOSTIC_INTERVAL_SEC = 30; // background run interval + private static final long DIAGNOSTIC_GUARD_SEC = 25; // safety dedup guard private final DataSource dataSource; private final RedisConnectionFactory redisConnectionFactory; - // --- CORRECT CONSTRUCTOR START --- + private final ScheduledExecutorService diagnosticScheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "mysql-diagnostic-thread"); + t.setDaemon(true); + return t; + }); + + private final AtomicLong lastDiagnosticRunAt = new AtomicLong(0); + private final AtomicReference cachedDbSeverity = + new AtomicReference<>("INFO"); + private final AtomicLong previousDeadlockCount = new AtomicLong(0); public HealthService(ObjectProvider dataSourceProvider, ObjectProvider redisProvider) { - // This allows them to be null without crashing the app this.dataSource = dataSourceProvider.getIfAvailable(); this.redisConnectionFactory = redisProvider.getIfAvailable(); + + // Start background diagnostics only if DB is configured. + // Initial delay = 0 so the first run happens at startup. + if (this.dataSource != null) { + diagnosticScheduler.scheduleAtFixedRate( + this::runAdvancedMySQLDiagnostics, + 0, + DIAGNOSTIC_INTERVAL_SEC, + TimeUnit.SECONDS + ); + } } - // --- CORRECT CONSTRUCTOR END --- + @PreDestroy + public void shutdownDiagnostics() { + logger.info("[HEALTH_SERVICE_SHUTDOWN] Shutting down diagnostic scheduler..."); + diagnosticScheduler.shutdown(); + try { + if (!diagnosticScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler did not terminate gracefully"); + diagnosticScheduler.shutdownNow(); + } + logger.info("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler shut down successfully"); + } catch (InterruptedException e) { + logger.error("[HEALTH_SERVICE_SHUTDOWN] Interrupted while shutting down scheduler", e); + diagnosticScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // PUBLIC — Called by the /health controller public Map checkHealth() { - Map response = new HashMap<>(); - Map components = new HashMap<>(); + Map response = new LinkedHashMap<>(); - boolean dbUp = checkDatabase(components); - boolean redisUp = checkRedis(components); + Map mysqlResult = checkDatabaseConnectivity(); + Map redisResult = checkRedisConnectivity(); - boolean overallUp = dbUp && redisUp; + String mysqlStatus = (String) mysqlResult.get("status"); + String redisStatus = (String) redisResult.get("status"); - response.put("status", overallUp ? "UP" : "DOWN"); - response.put("components", components); + boolean overallUp = !"DOWN".equals(mysqlStatus) && !"DOWN".equals(redisStatus); + + response.put("status", overallUp ? "UP" : "DOWN"); + response.put("checkedAt", Instant.now().toString()); + + // Expose only status and severity, keep diagnostics internal + Map mysqlSummary = new LinkedHashMap<>(); + mysqlSummary.put("status", mysqlResult.get("status")); + mysqlSummary.put("severity", mysqlResult.get("severity")); + + Map redisSummary = new LinkedHashMap<>(); + redisSummary.put("status", redisResult.get("status")); + redisSummary.put("severity", redisResult.get("severity")); + + response.put("mysql", mysqlSummary); + response.put("redis", redisSummary); return response; } + // Runs only SELECT 1 with a hard 3-second timeout. + private Map checkDatabaseConnectivity() { + Map result = new LinkedHashMap<>(); - private boolean checkDatabase(Map components) { if (dataSource == null) { - components.put(COMPONENT_DATABASE, "NOT_CONFIGURED"); - return true; + result.put("status", "NOT_CONFIGURED"); + result.put("severity", "INFO"); + return result; } - try (Connection connection = dataSource.getConnection(); - var statement = connection.createStatement()) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.setQueryTimeout(3); // Hard cap — /health must never block > 3s + stmt.execute("SELECT 1"); - statement.execute("SELECT 1"); - components.put(COMPONENT_DATABASE, "UP"); - return true; + // If SELECT 1 succeeds, use cached severity from background diagnostics + String severity = cachedDbSeverity.get(); + result.put("status", resolveDatabaseStatus(severity)); + result.put("severity", severity); } catch (Exception e) { - logger.error("Database health check failed", e); - components.put(COMPONENT_DATABASE, "DOWN"); - return false; + // Log connection failure as a structured event + logger.error( + "[MYSQL_CONNECT_FAILED] MySQL connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put("status", "DOWN"); + result.put("severity", "CRITICAL"); } + + return result; } - private boolean checkRedis(Map components) { + private Map checkRedisConnectivity() { + Map result = new LinkedHashMap<>(); + if (redisConnectionFactory == null) { - components.put(COMPONENT_REDIS, "NOT_CONFIGURED"); - return true; + result.put("status", "NOT_CONFIGURED"); + result.put("severity", "INFO"); + return result; } - try (RedisConnection connection = redisConnectionFactory.getConnection()) { - connection.ping(); - components.put(COMPONENT_REDIS, "UP"); - return true; + try (RedisConnection conn = redisConnectionFactory.getConnection()) { + conn.ping(); + result.put("status", "UP"); + result.put("severity", "OK"); } catch (Exception e) { - logger.error("Redis health check failed", e); - components.put(COMPONENT_REDIS, "DOWN"); - return false; + logger.error( + "[REDIS_CONNECT_FAILED] Redis connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put("status", "DOWN"); + result.put("severity", "CRITICAL"); + } + + return result; + } + + private void runAdvancedMySQLDiagnostics() { + // Dedup guard: skip if last run was within the past 25 seconds + long now = System.currentTimeMillis(); + if (now - lastDiagnosticRunAt.get() < TimeUnit.SECONDS.toMillis(DIAGNOSTIC_GUARD_SEC)) { + return; } + lastDiagnosticRunAt.set(now); + + String worstSeverity = "INFO"; // Escalates during checks, never descends + + try (Connection conn = dataSource.getConnection()) { + + // CHECK 1 — Stuck / Long-Running Processes + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + + "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { + + if (rs.next()) { + int stuckCount = rs.getInt("cnt"); + if (stuckCount > 0) { + logger.warn( + "[{}] Stuck MySQL processes detected | count={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_SECONDS + ); + if (stuckCount > STUCK_PROCESS_THRESHOLD) { + worstSeverity = escalate(worstSeverity, "WARNING"); + } + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", + e.getMessage()); + } + + // CHECK 2 — InnoDB Long-Running Transactions (MYSQL_LONG_TX) + // Note: INNODB_TRX shows all active transactions. True lock-wait detection via + // INNODB_LOCK_WAITS requires PERFORMANCE_SCHEMA enabled and explicit permissions. + // This query flags transactions older than STUCK_PROCESS_SECONDS as potentially problematic. + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + STUCK_PROCESS_SECONDS)) { + + if (rs.next()) { + int lockCount = rs.getInt("cnt"); + if (lockCount > 0) { + logger.error( + "[{}] InnoDB long-running transaction detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, STUCK_PROCESS_SECONDS + ); + worstSeverity = escalate(worstSeverity, "CRITICAL"); + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", + e.getMessage()); + } + + // CHECK 3 — InnoDB Deadlocks (Delta Tracking to avoid permanent WARNING) + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { + + if (rs.next()) { + long currentDeadlocks = rs.getLong("Value"); + long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); + + // Only warn if deadlocks have *increased* since last run + if (currentDeadlocks > previousDeadlocks) { + long deltaDeadlocks = currentDeadlocks - previousDeadlocks; + logger.warn( + "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks + ); + worstSeverity = escalate(worstSeverity, "WARNING"); + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", + e.getMessage()); + } + + // CHECK 4 — Slow Queries + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + + if (rs.next()) { + long slowQueries = rs.getLong("Value"); + if (slowQueries > 0) { + logger.warn( + "[{}] Slow queries detected | cumulativeCount={}", + LOG_EVENT_SLOW_QUERIES, slowQueries + ); + worstSeverity = escalate(worstSeverity, "WARNING"); + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", + e.getMessage()); + } + + // CHECK 5 — Server Connection Usage + try (Statement stmt = conn.createStatement()) { + int threadsConnected = 0; + int maxConnections = 0; + + try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { + if (rs.next()) threadsConnected = rs.getInt("Value"); + } + + try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { + if (rs.next()) maxConnections = rs.getInt("Value"); + } + + if (maxConnections > 0) { + int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); + + if (usagePct >= CONNECTION_USAGE_CRITICAL) { + logger.error( + "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct + ); + worstSeverity = escalate(worstSeverity, "CRITICAL"); + + } else if (usagePct > CONNECTION_USAGE_WARNING) { + logger.warn( + "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct + ); + worstSeverity = escalate(worstSeverity, "WARNING"); + } + } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", + e.getMessage()); + } + + } catch (Exception e) { + // Cannot open connection for diagnostics — treat as CRITICAL + logger.error( + "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", + e.getMessage() + ); + worstSeverity = "CRITICAL"; + } + + // Persist computed severity so /health can read it instantly + cachedDbSeverity.set(worstSeverity); + + logger.debug( + "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", + worstSeverity + ); + } + private String resolveDatabaseStatus(String severity) { + return switch (severity) { + case "CRITICAL" -> "DOWN"; + case "WARNING" -> "DEGRADED"; + default -> "UP"; + }; + } + private String escalate(String current, String candidate) { + return severityRank(candidate) > severityRank(current) ? candidate : current; + } + + private int severityRank(String severity) { + return switch (severity) { + case "CRITICAL" -> 2; + case "WARNING" -> 1; + default -> 0; + }; } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 81d79221..364aa12d 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -251,7 +251,9 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { || path.startsWith(contextPath + "/user/userLogout") || path.startsWith(contextPath + "/user/validateSecurityQuestionAndAnswer") || path.startsWith(contextPath + "/user/logOutUserFromConcurrentSession") - || path.startsWith(contextPath + "/user/refreshToken"); + || path.startsWith(contextPath + "/user/refreshToken") + || path.equals(contextPath + "/health") + || path.equals(contextPath + "/version"); } private String getJwtTokenFromCookies(HttpServletRequest request) { From bfe5ab3fe88c5b4ac50af94ae4ae7b8178742e35 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 23 Feb 2026 11:15:14 +0530 Subject: [PATCH 12/14] fix(health): normalize severity and fix slow query false positives --- .../common/service/health/HealthService.java | 336 ++++++++++-------- 1 file changed, 186 insertions(+), 150 deletions(-) diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index d117337e..894013a2 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -47,12 +47,35 @@ public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); + // Event log constants private static final String LOG_EVENT_STUCK_PROCESS = "MYSQL_STUCK_PROCESS"; private static final String LOG_EVENT_LOCK_WAIT = "MYSQL_LOCK_WAIT"; private static final String LOG_EVENT_DEADLOCK = "MYSQL_DEADLOCK"; private static final String LOG_EVENT_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; private static final String LOG_EVENT_CONN_USAGE = "MYSQL_CONNECTION_USAGE"; private static final String LOG_EVENT_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + + // Response field constants + private static final String FIELD_STATUS = "status"; + private static final String FIELD_SEVERITY = "severity"; + private static final String FIELD_MYSQL = "mysql"; + private static final String FIELD_REDIS = "redis"; + private static final String FIELD_CHECKED_AT = "checkedAt"; + + // Severity constants + private static final String SEVERITY_CRITICAL = "CRITICAL"; + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_INFO = "INFO"; + + // Database query constants + private static final String STATUS_VALUE = "Value"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + private static final String STATUS_NOT_CONFIGURED = "NOT_CONFIGURED"; + + // Thresholds private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds @@ -72,8 +95,9 @@ public class HealthService { private final AtomicLong lastDiagnosticRunAt = new AtomicLong(0); private final AtomicReference cachedDbSeverity = - new AtomicReference<>("INFO"); + new AtomicReference<>(SEVERITY_OK); private final AtomicLong previousDeadlockCount = new AtomicLong(0); + private final AtomicLong previousSlowQueryCount = new AtomicLong(0); public HealthService(ObjectProvider dataSourceProvider, ObjectProvider redisProvider) { this.dataSource = dataSourceProvider.getIfAvailable(); @@ -115,25 +139,25 @@ public Map checkHealth() { Map mysqlResult = checkDatabaseConnectivity(); Map redisResult = checkRedisConnectivity(); - String mysqlStatus = (String) mysqlResult.get("status"); - String redisStatus = (String) redisResult.get("status"); + String mysqlStatus = (String) mysqlResult.get(FIELD_STATUS); + String redisStatus = (String) redisResult.get(FIELD_STATUS); - boolean overallUp = !"DOWN".equals(mysqlStatus) && !"DOWN".equals(redisStatus); + boolean overallUp = !STATUS_DOWN.equals(mysqlStatus) && !STATUS_DOWN.equals(redisStatus); - response.put("status", overallUp ? "UP" : "DOWN"); - response.put("checkedAt", Instant.now().toString()); + response.put(FIELD_STATUS, overallUp ? STATUS_UP : STATUS_DOWN); + response.put(FIELD_CHECKED_AT, Instant.now().toString()); // Expose only status and severity, keep diagnostics internal Map mysqlSummary = new LinkedHashMap<>(); - mysqlSummary.put("status", mysqlResult.get("status")); - mysqlSummary.put("severity", mysqlResult.get("severity")); + mysqlSummary.put(FIELD_STATUS, mysqlResult.get(FIELD_STATUS)); + mysqlSummary.put(FIELD_SEVERITY, mysqlResult.get(FIELD_SEVERITY)); Map redisSummary = new LinkedHashMap<>(); - redisSummary.put("status", redisResult.get("status")); - redisSummary.put("severity", redisResult.get("severity")); + redisSummary.put(FIELD_STATUS, redisResult.get(FIELD_STATUS)); + redisSummary.put(FIELD_SEVERITY, redisResult.get(FIELD_SEVERITY)); - response.put("mysql", mysqlSummary); - response.put("redis", redisSummary); + response.put(FIELD_MYSQL, mysqlSummary); + response.put(FIELD_REDIS, redisSummary); return response; } @@ -142,8 +166,8 @@ private Map checkDatabaseConnectivity() { Map result = new LinkedHashMap<>(); if (dataSource == null) { - result.put("status", "NOT_CONFIGURED"); - result.put("severity", "INFO"); + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); return result; } @@ -155,8 +179,8 @@ private Map checkDatabaseConnectivity() { // If SELECT 1 succeeds, use cached severity from background diagnostics String severity = cachedDbSeverity.get(); - result.put("status", resolveDatabaseStatus(severity)); - result.put("severity", severity); + result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_SEVERITY, severity); } catch (Exception e) { // Log connection failure as a structured event @@ -165,8 +189,8 @@ private Map checkDatabaseConnectivity() { e.getMessage() ); - result.put("status", "DOWN"); - result.put("severity", "CRITICAL"); + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } return result; @@ -176,15 +200,15 @@ private Map checkRedisConnectivity() { Map result = new LinkedHashMap<>(); if (redisConnectionFactory == null) { - result.put("status", "NOT_CONFIGURED"); - result.put("severity", "INFO"); + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); return result; } try (RedisConnection conn = redisConnectionFactory.getConnection()) { conn.ping(); - result.put("status", "UP"); - result.put("severity", "OK"); + result.put(FIELD_STATUS, STATUS_UP); + result.put(FIELD_SEVERITY, SEVERITY_OK); } catch (Exception e) { logger.error( @@ -192,8 +216,8 @@ private Map checkRedisConnectivity() { e.getMessage() ); - result.put("status", "DOWN"); - result.put("severity", "CRITICAL"); + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } return result; @@ -207,157 +231,169 @@ private void runAdvancedMySQLDiagnostics() { } lastDiagnosticRunAt.set(now); - String worstSeverity = "INFO"; // Escalates during checks, never descends + String worstSeverity = SEVERITY_OK; try (Connection conn = dataSource.getConnection()) { + worstSeverity = escalate(worstSeverity, performStuckProcessCheck(conn)); + worstSeverity = escalate(worstSeverity, performLongTransactionCheck(conn)); + worstSeverity = escalate(worstSeverity, performDeadlockCheck(conn)); + worstSeverity = escalate(worstSeverity, performSlowQueryCheck(conn)); + worstSeverity = escalate(worstSeverity, performConnectionUsageCheck(conn)); - // CHECK 1 — Stuck / Long-Running Processes - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + - "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { - - if (rs.next()) { - int stuckCount = rs.getInt("cnt"); - if (stuckCount > 0) { - logger.warn( - "[{}] Stuck MySQL processes detected | count={} | thresholdSeconds={}", - LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_SECONDS - ); - if (stuckCount > STUCK_PROCESS_THRESHOLD) { - worstSeverity = escalate(worstSeverity, "WARNING"); - } + } catch (Exception e) { + logger.error( + "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", + e.getMessage() + ); + worstSeverity = SEVERITY_CRITICAL; + } + + cachedDbSeverity.set(worstSeverity); + logger.debug( + "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", + worstSeverity + ); + } + + private String performStuckProcessCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + + "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { + + if (rs.next()) { + int stuckCount = rs.getInt("cnt"); + if (stuckCount > 0) { + logger.warn( + "[{}] Stuck MySQL processes detected | count={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_SECONDS + ); + if (stuckCount > STUCK_PROCESS_THRESHOLD) { + return SEVERITY_WARNING; } } - } catch (Exception e) { - logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", - e.getMessage()); } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } - // CHECK 2 — InnoDB Long-Running Transactions (MYSQL_LONG_TX) - // Note: INNODB_TRX shows all active transactions. True lock-wait detection via - // INNODB_LOCK_WAITS requires PERFORMANCE_SCHEMA enabled and explicit permissions. - // This query flags transactions older than STUCK_PROCESS_SECONDS as potentially problematic. - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + - "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + STUCK_PROCESS_SECONDS)) { - - if (rs.next()) { - int lockCount = rs.getInt("cnt"); - if (lockCount > 0) { - logger.error( - "[{}] InnoDB long-running transaction detected | count={} | thresholdSeconds={}", - LOG_EVENT_LOCK_WAIT, lockCount, STUCK_PROCESS_SECONDS - ); - worstSeverity = escalate(worstSeverity, "CRITICAL"); - } + private String performLongTransactionCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + STUCK_PROCESS_SECONDS)) { + + if (rs.next()) { + int lockCount = rs.getInt("cnt"); + if (lockCount > 0) { + logger.error( + "[{}] InnoDB long-running transaction detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, STUCK_PROCESS_SECONDS + ); + return SEVERITY_CRITICAL; } - } catch (Exception e) { - logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", - e.getMessage()); } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } - // CHECK 3 — InnoDB Deadlocks (Delta Tracking to avoid permanent WARNING) - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { + private String performDeadlockCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { + + if (rs.next()) { + long currentDeadlocks = rs.getLong(STATUS_VALUE); + long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); - if (rs.next()) { - long currentDeadlocks = rs.getLong("Value"); - long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); - - // Only warn if deadlocks have *increased* since last run - if (currentDeadlocks > previousDeadlocks) { - long deltaDeadlocks = currentDeadlocks - previousDeadlocks; - logger.warn( - "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", - LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks - ); - worstSeverity = escalate(worstSeverity, "WARNING"); - } + if (currentDeadlocks > previousDeadlocks) { + long deltaDeadlocks = currentDeadlocks - previousDeadlocks; + logger.warn( + "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks + ); + return SEVERITY_WARNING; } - } catch (Exception e) { - logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", - e.getMessage()); } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } - // CHECK 4 — Slow Queries - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + private String performSlowQueryCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + + if (rs.next()) { + long slowQueries = rs.getLong(STATUS_VALUE); + long previousSlow = previousSlowQueryCount.getAndSet(slowQueries); - if (rs.next()) { - long slowQueries = rs.getLong("Value"); - if (slowQueries > 0) { - logger.warn( - "[{}] Slow queries detected | cumulativeCount={}", - LOG_EVENT_SLOW_QUERIES, slowQueries - ); - worstSeverity = escalate(worstSeverity, "WARNING"); - } + // Only warn if slow queries have *increased* since last run + if (slowQueries > previousSlow) { + long delta = slowQueries - previousSlow; + logger.warn( + "[{}] New slow queries detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_SLOW_QUERIES, delta, slowQueries + ); + return SEVERITY_WARNING; } - } catch (Exception e) { - logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", - e.getMessage()); } + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", + e.getMessage()); + } + return SEVERITY_OK; + } - // CHECK 5 — Server Connection Usage - try (Statement stmt = conn.createStatement()) { - int threadsConnected = 0; - int maxConnections = 0; + private String performConnectionUsageCheck(Connection conn) { + try (Statement stmt = conn.createStatement()) { + int threadsConnected = 0; + int maxConnections = 0; - try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { - if (rs.next()) threadsConnected = rs.getInt("Value"); - } + try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { + if (rs.next()) threadsConnected = rs.getInt(STATUS_VALUE); + } - try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { - if (rs.next()) maxConnections = rs.getInt("Value"); - } + try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { + if (rs.next()) maxConnections = rs.getInt(STATUS_VALUE); + } - if (maxConnections > 0) { - int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); - - if (usagePct >= CONNECTION_USAGE_CRITICAL) { - logger.error( - "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", - LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct - ); - worstSeverity = escalate(worstSeverity, "CRITICAL"); - - } else if (usagePct > CONNECTION_USAGE_WARNING) { - logger.warn( - "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", - LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct - ); - worstSeverity = escalate(worstSeverity, "WARNING"); - } + if (maxConnections > 0) { + int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); + + if (usagePct >= CONNECTION_USAGE_CRITICAL) { + logger.error( + "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_CRITICAL; + + } else if (usagePct > CONNECTION_USAGE_WARNING) { + logger.warn( + "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_WARNING; } - } catch (Exception e) { - logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", - e.getMessage()); } - } catch (Exception e) { - // Cannot open connection for diagnostics — treat as CRITICAL - logger.error( - "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", - e.getMessage() - ); - worstSeverity = "CRITICAL"; + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", + e.getMessage()); } - - // Persist computed severity so /health can read it instantly - cachedDbSeverity.set(worstSeverity); - - logger.debug( - "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", - worstSeverity - ); + return SEVERITY_OK; } private String resolveDatabaseStatus(String severity) { return switch (severity) { - case "CRITICAL" -> "DOWN"; - case "WARNING" -> "DEGRADED"; - default -> "UP"; + case SEVERITY_CRITICAL -> STATUS_DOWN; + case SEVERITY_WARNING -> STATUS_DEGRADED; + default -> STATUS_UP; }; } private String escalate(String current, String candidate) { @@ -366,9 +402,9 @@ private String escalate(String current, String candidate) { private int severityRank(String severity) { return switch (severity) { - case "CRITICAL" -> 2; - case "WARNING" -> 1; - default -> 0; + case SEVERITY_CRITICAL -> 2; + case SEVERITY_WARNING -> 1; + default -> 0; }; } } \ No newline at end of file From d43d892e320187392141d344a10cfbb4bff6820c Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 23 Feb 2026 11:29:01 +0530 Subject: [PATCH 13/14] fix(health): avoid false CRITICAL on single long-running MySQL transaction --- .../common/service/health/HealthService.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index 894013a2..532b0302 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -79,6 +79,9 @@ public class HealthService { private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds + private static final int LONG_TXN_WARNING_THRESHOLD = 1; // ≥1 long txn → WARNING + private static final int LONG_TXN_CRITICAL_THRESHOLD = 5; // ≥5 long txns → CRITICAL + private static final int LONG_TXN_SECONDS = 60; // transaction age threshold private static final int CONNECTION_USAGE_WARNING = 80; // > 80% → WARNING private static final int CONNECTION_USAGE_CRITICAL= 95; // > 95% → CRITICAL private static final long DIAGNOSTIC_INTERVAL_SEC = 30; // background run interval @@ -284,16 +287,18 @@ private String performLongTransactionCheck(Connection conn) { try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + - "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + STUCK_PROCESS_SECONDS)) { + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + LONG_TXN_SECONDS)) { if (rs.next()) { int lockCount = rs.getInt("cnt"); - if (lockCount > 0) { - logger.error( - "[{}] InnoDB long-running transaction detected | count={} | thresholdSeconds={}", - LOG_EVENT_LOCK_WAIT, lockCount, STUCK_PROCESS_SECONDS + if (lockCount >= LONG_TXN_WARNING_THRESHOLD) { + logger.warn( + "[{}] InnoDB long-running transaction(s) detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, LONG_TXN_SECONDS ); - return SEVERITY_CRITICAL; + // Graduated escalation: WARNING for 1-4, CRITICAL for 5+ + return lockCount >= LONG_TXN_CRITICAL_THRESHOLD + ? SEVERITY_CRITICAL : SEVERITY_WARNING; } } } catch (Exception e) { From d549ad583b7cb6424b2ff6ab3718bd86a33dae41 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 23 Feb 2026 11:46:52 +0530 Subject: [PATCH 14/14] fix(health): enforce 3s DB connection timeout via HikariCP --- .../common/service/health/HealthService.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index 532b0302..7714efce 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -28,7 +28,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Service; -import javax.annotation.PreDestroy; +import jakarta.annotation.PreDestroy; import javax.sql.DataSource; import java.sql.Connection; import java.sql.ResultSet; @@ -164,7 +164,10 @@ public Map checkHealth() { return response; } - // Runs only SELECT 1 with a hard 3-second timeout. + // Runs only SELECT 1 with a hard 3-second timeout on query execution. + // NOTE: getConnection() is NOT bounded by this timeout — it respects the pool's + // connectionTimeout (default 30s in HikariCP). For true 3-second /health guarantees, + // configure the DataSource connectionTimeout ≤ 3 seconds or wrap in an ExecutorService timeout. private Map checkDatabaseConnectivity() { Map result = new LinkedHashMap<>(); @@ -177,7 +180,7 @@ private Map checkDatabaseConnectivity() { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { - stmt.setQueryTimeout(3); // Hard cap — /health must never block > 3s + stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution stmt.execute("SELECT 1"); // If SELECT 1 succeeds, use cached severity from background diagnostics @@ -267,12 +270,17 @@ private String performStuckProcessCheck(Connection conn) { if (rs.next()) { int stuckCount = rs.getInt("cnt"); if (stuckCount > 0) { - logger.warn( - "[{}] Stuck MySQL processes detected | count={} | thresholdSeconds={}", - LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_SECONDS - ); if (stuckCount > STUCK_PROCESS_THRESHOLD) { + logger.warn( + "[{}] Stuck MySQL processes detected above threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); return SEVERITY_WARNING; + } else { + logger.info( + "[{}] Stuck MySQL processes below threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); } } }