diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/apistatus/ApiErrorRateRow.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/apistatus/ApiErrorRateRow.java new file mode 100644 index 0000000..ce14c8e --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/apistatus/ApiErrorRateRow.java @@ -0,0 +1,41 @@ +package com.eactive.eai.rms.data.entity.onl.djb.apistatus; + +import java.math.BigDecimal; + +/** + * 판정 구간 내 오류가 1건 이상 집계된 API 의 오류율 (개발 모드 진단 로그용) + * + *

{@code ApiStatusRepository.findApiErrorRates} 결과. 장애 판정 임계값과 무관하게 + * 오류율 0% 초과인 API 를 모두 담는다.

+ */ +public interface ApiErrorRateRow { + + /** API(EAI 서비스)명 */ + String getEaisvcname(); + + /** 현재 상태 코드 - 정상(N), 점검(C), 지연(D), 장애(E) */ + String getStatusCode(); + + /** 판정 구간 내 총 건수 (성공 + 타임아웃 + 시스템오류 + 업무오류) */ + BigDecimal getTotal(); + + /** 판정 구간 내 성공 건수 */ + BigDecimal getSuccess(); + + /** 오류율(%) - (총건수 - 성공건수) / 총건수 * 100 */ + BigDecimal getErrorRate(); + + /** 오류 판정 구간 내 건수 가중 평균 응답시간(ms) */ + BigDecimal getAvgRespTime(); + + /** + * 지연 판정 구간 내 총 건수. + * + *

지연은 오류와 판정 구간이 다르다({@code api.status.delay.range_minute}). + * 지연 임계 비교는 이 구간의 평균 응답시간으로 해야 실제 판정과 일치한다.

+ */ + BigDecimal getDelayTotal(); + + /** 지연 판정 구간 내 건수 가중 평균 응답시간(ms) */ + BigDecimal getDelayAvgRespTime(); +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java index ee8445d..32a2fa5 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param; import java.util.Collection; import com.eactive.eai.data.jpa.BaseRepository; +import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent; @@ -26,8 +27,9 @@ public interface ApiStatusRepository extends BaseRepository { */ @Query(nativeQuery = true, value = " SELECT EAISVCNAME" - + " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" - + " , PREV_STATUS_CODE " + + " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" + // 따옴표 없는 PREV_STATUS_CODE 는 Oracle 이 대문자로 올려 getPrevStatusCode() 와 매칭되지 않는다 (null 반환) + + " , PREV_STATUS_CODE AS \"prevStatusCode\"" + " , EVENT" + " FROM (" + " SELECT EAISVCNAME" @@ -80,6 +82,54 @@ public interface ApiStatusRepository extends BaseRepository { @Param("delayAvgRespTime") int delayAvgRespTime ); + /** + * 판정 구간 내 오류율 0% 초과 API 목록. 개발 모드(-Deai.systemmode=D) 진단 로그 전용. + * + *

{@code findApiStatusEvents} 는 임계값을 넘고 상태 전이까지 일어난 API 만 돌려주므로 + * "오류는 나는데 왜 장애로 안 잡히나" 를 볼 수 없다. 이 쿼리는 임계값·현재 상태와 무관하게 + * 집계된 오류율을 그대로 보여준다.

+ * + *

컬럼 alias 는 반드시 따옴표로 감싼 camelCase 로 둔다. 인터페이스 프로젝션은 tuple + * alias 를 getter 프로퍼티명으로 찾는데, 따옴표가 없으면 Oracle 이 alias 를 대문자로 올려 + * {@code STATUS_CODE} 와 {@code statusCode} 가 매칭되지 않아 조용히 null 이 된다 + * (단일 단어는 대소문자만 달라 우연히 매칭된다).

+ */ + @Query(nativeQuery = true, value = + " SELECT S.API_NAME AS \"eaisvcname\"" + + " , NVL(B.STATUS_CODE,'N') AS \"statusCode\"" + + " , S.TOTAL AS \"total\"" + + " , S.SUCCESS AS \"success\"" + + " , ROUND((S.TOTAL - S.SUCCESS) * 100 / S.TOTAL, 2) AS \"errorRate\"" + + " , ROUND(S.RESP_SUM / S.TOTAL, 2) AS \"avgRespTime\"" + + " , NVL(D.TOTAL,0) AS \"delayTotal\"" + + " , CASE WHEN NVL(D.TOTAL,0) > 0 THEN ROUND(D.RESP_SUM / D.TOTAL, 2) END AS \"delayAvgRespTime\"" + + " FROM (SELECT API_NAME" + + " , NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL" + + " , NVL(SUM(SUCCESS_CNT),0) AS SUCCESS" + + " , NVL(SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME),0) AS RESP_SUM" + + " FROM API_STATS_MINUTE" + + " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')" + + " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')" + + " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')" + + " GROUP BY API_NAME) S" + // 지연은 판정 구간이 따로다(delayRangeMinute). 오류 구간 평균으로 비교하면 실제 판정과 어긋난다 + + " LEFT OUTER JOIN (SELECT API_NAME" + + " , NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL" + + " , NVL(SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME),0) AS RESP_SUM" + + " FROM API_STATS_MINUTE" + + " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')" + + " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:delayRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')" + + " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')" + + " GROUP BY API_NAME) D ON S.API_NAME = D.API_NAME" + + " LEFT OUTER JOIN API_STATUS B ON S.API_NAME = B.EAISVCNAME" + + " WHERE S.TOTAL > 0" + + " AND S.TOTAL - S.SUCCESS > 0" + + " ORDER BY (S.TOTAL - S.SUCCESS) / S.TOTAL DESC, S.API_NAME") + List findApiErrorRates( + @Param("errorRangeMinute") int errorRangeMinute, + @Param("delayRangeMinute") int delayRangeMinute + ); + /** * EAI 서비스명 조회. 장애/점검 영향 API 의 API_NAME 캐시용. */ diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java index 8e2f2fc..3587df3 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java @@ -1,5 +1,7 @@ package com.eactive.eai.rms.ext.djb.apistatus; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -13,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional; import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.eai.rms.data.entity.man.role.Role; +import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent; import com.eactive.eai.rms.ext.djb.ums.UmsManager; @@ -35,14 +38,21 @@ public class ApiStatusService { public void updateApiStatus(HashMap param) { - + + int errorRate = Integer.parseInt(param.get("errorRate")); + int errorRangeMinute = Integer.parseInt(param.get("errorRangeMinute")); + int delayRangeMinute = Integer.parseInt(param.get("delayRangeMinute")); + int delayAvgRespTime = Integer.parseInt(param.get("delayAvgRespTime")); + + logErrorRates(errorRate, errorRangeMinute, delayRangeMinute, delayAvgRespTime); + List list = apiStatusRepository.findApiStatusEvents( - Integer.parseInt(param.get("errorRate")), - Integer.parseInt(param.get("errorRangeMinute")), - Integer.parseInt(param.get("delayRangeMinute")), - Integer.parseInt(param.get("delayAvgRespTime")) + errorRate, + errorRangeMinute, + delayRangeMinute, + delayAvgRespTime ); - + HashMap> eventMap = new HashMap<>(); @@ -88,6 +98,81 @@ public class ApiStatusService { } + /** + * 오류율 0% 초과 API 목록을 debug 로그로 남긴다. 개발 모드(-Deai.systemmode=D) 에서만 동작한다. + * + *

상태 전이 조건(정상/지연 → 장애)을 못 넘긴 API 는 {@code findApiStatusEvents} 결과에 + * 나오지 않아 "오류는 나는데 장애로 안 잡힌다" 를 로그로 확인할 수 없다. 임계값·현재 상태와 + * 무관하게 집계된 오류율을 그대로 찍어 판정 결과와 대조할 수 있게 한다.

+ * + *

진단 목적이므로 조회 실패가 상태 판정 자체를 막아서는 안 된다 - 예외는 삼킨다.

+ */ + private void logErrorRates(int errorRate, int errorRangeMinute, int delayRangeMinute, int delayAvgRespTime) { + if (!isDevMode() || !log.isDebugEnabled()) { + return; + } + + try { + List rows = apiStatusRepository.findApiErrorRates(errorRangeMinute, delayRangeMinute); + if (rows.isEmpty()) { + log.debug("오류율 탐지: 최근 {}분간 오류 발생 API 없음", errorRangeMinute); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append("오류율 탐지: 장애 임계 ").append(errorRate).append("% 초과(최근 ").append(errorRangeMinute) + .append("분), 지연 임계 ").append(delayAvgRespTime).append("ms 초과(최근 ").append(delayRangeMinute) + .append("분), 대상 ").append(rows.size()).append("건"); + + for (ApiErrorRateRow row : rows) { + sb.append("\n - ").append(row.getEaisvcname()) + .append(" 현재상태=").append(row.getStatusCode()) + .append("\n 오류율=").append(row.getErrorRate()).append("%") + .append(" (임계 대비 ").append(ratioOf(row.getErrorRate(), errorRate)).append("%)") + .append(", 총 ").append(row.getTotal()).append("건, 성공 ").append(row.getSuccess()).append("건") + .append(" → ").append(thresholdMark(row.getErrorRate(), errorRate)) + .append("\n 평균응답=").append(row.getDelayAvgRespTime()).append("ms") + .append(" (임계 대비 ").append(ratioOf(row.getDelayAvgRespTime(), delayAvgRespTime)).append("%)") + .append(", 총 ").append(row.getDelayTotal()).append("건") + .append(" → ").append(thresholdMark(row.getDelayAvgRespTime(), delayAvgRespTime)); + } + log.debug(sb.toString()); + + } catch (Exception e) { + log.debug("오류율 탐지 로그 생성 실패", e); + } + } + + /** + * 임계값 대비 비율(%). 100% 면 임계값과 같은 값이고, 판정은 '초과'(>) 라 100% 는 아직 안 걸린다. + */ + private String ratioOf(BigDecimal value, int threshold) { + if (value == null || threshold <= 0) { + return "-"; + } + return value.multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(threshold), 2, RoundingMode.HALF_UP) + .toPlainString(); + } + + /** + * 임계 초과 여부 표시. 판정 조건이 '초과'(>) 이므로 임계값과 같은 값은 초과가 아니다. + */ + private String thresholdMark(BigDecimal value, int threshold) { + if (value == null) { + return "데이터없음"; + } + return value.compareTo(BigDecimal.valueOf(threshold)) > 0 ? "임계초과" : "임계이하"; + } + + /** + * 개발 모드 여부. -Deai.systemmode=D 로 기동한 경우에만 진단 로그를 남긴다. + */ + private boolean isDevMode() { + return "D".equalsIgnoreCase(System.getProperty("eai.systemmode", "")); + } + + private String resolveStatusCode(String event) { switch (event) { case "CONTROL_START": return "C"; //점검 diff --git a/src/main/resources/logback-rinjae_ma.xml b/src/main/resources/logback-rinjae_ma.xml index 6cf3f3d..e5fe24b 100644 --- a/src/main/resources/logback-rinjae_ma.xml +++ b/src/main/resources/logback-rinjae_ma.xml @@ -125,7 +125,8 @@ - + +