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 @@
-
+
+