API 상태 판정 로직 개선
eapim-admin CI / build (push) Has been cancelled

- 오류율 및 지연 판정 쿼리 조건 수정
- 상태 판정 진단 로그 메시지 포맷 및 로직 업데이트
This commit is contained in:
Rinjae
2026-08-04 19:18:43 +09:00
parent 46bd79bc36
commit 3bf8370d0c
2 changed files with 35 additions and 22 deletions
@@ -83,11 +83,13 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
);
/**
* 판정 구간 내 오류율 0% 초과 API 목록. 개발 모드(-Deai.systemmode=D) 진단 로그 전용.
* 판정 구간에 트래픽이 있는 API 의 오류율·지연 판정값. 개발 모드(-Deai.systemmode=D) 진단 로그 전용.
*
* <p>{@code findApiStatusEvents} 는 임계값을 넘고 <b>상태 전이까지 일어난</b> API 만 돌려주므로
* "오류는 나는데 왜 장애로 안 잡히나" 를 볼 수 없다. 이 쿼리는 임계값·현재 상태와 무관하게
* 집계된 오류율을 그대로 보여준다.</p>
* "오류는 나는데 왜 장애로 안 잡히나", "느린데 왜 지연이 아닌가" 를 볼 수 없다. 이 쿼리는
* 임계값·현재 상태와 무관하게 집계값을 그대로 보여준다.</p>
*
* <p>오류가 0건이어도 지연 판정값은 확인해야 하므로 오류 발생 여부로 거르지 않는다.</p>
*
* <p>컬럼 alias 는 반드시 <b>따옴표로 감싼 camelCase</b> 로 둔다. 인터페이스 프로젝션은 tuple
* alias 를 getter 프로퍼티명으로 찾는데, 따옴표가 없으면 Oracle 이 alias 를 대문자로 올려
@@ -95,15 +97,22 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
* (단일 단어는 대소문자만 달라 우연히 매칭된다).</p>
*/
@Query(nativeQuery = true, value =
" SELECT S.API_NAME AS \"eaisvcname\""
" SELECT N.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(S.TOTAL,0) AS \"total\""
+ " , NVL(S.SUCCESS,0) AS \"success\""
+ " , CASE WHEN NVL(S.TOTAL,0) > 0 THEN ROUND((S.TOTAL - S.SUCCESS) * 100 / S.TOTAL, 2) END AS \"errorRate\""
+ " , CASE WHEN NVL(S.TOTAL,0) > 0 THEN ROUND(S.RESP_SUM / S.TOTAL, 2) END 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"
// 두 창 중 넓은 쪽으로 대상 API 를 먼저 뽑는다. 오류 창만 기준으로 잡으면 오류 창에 거래가
// 없을 때 지연 판정값까지 통째로 사라진다 (창 길이가 서로 다르다)
+ " FROM (SELECT DISTINCT API_NAME"
+ " FROM API_STATS_MINUTE"
+ " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(GREATEST(:errorRangeMinute, :delayRangeMinute),'MINUTE'),'YYYYMMDDHH24MI')"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')) N"
+ " 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),0) AS SUCCESS"
+ " , NVL(SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME),0) AS RESP_SUM"
@@ -111,7 +120,7 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')"
+ " GROUP BY API_NAME) S"
+ " GROUP BY API_NAME) S ON N.API_NAME = S.API_NAME"
// 지연은 판정 구간이 따로다(delayRangeMinute). 오류 구간 평균으로 비교하면 실제 판정과 어긋난다
+ " LEFT OUTER JOIN (SELECT API_NAME"
+ " , NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL"
@@ -120,11 +129,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " 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")
+ " GROUP BY API_NAME) D ON N.API_NAME = D.API_NAME"
+ " LEFT OUTER JOIN API_STATUS B ON N.API_NAME = B.EAISVCNAME"
// 두 창 중 어느 쪽이든 거래가 있으면 찍는다 (오류 0건, 오류 창 무거래 모두 포함)
+ " WHERE NVL(S.TOTAL,0) > 0 OR NVL(D.TOTAL,0) > 0"
+ " ORDER BY CASE WHEN NVL(S.TOTAL,0) > 0 THEN (S.TOTAL - S.SUCCESS) / S.TOTAL ELSE -1 END DESC, N.API_NAME")
List<ApiErrorRateRow> findApiErrorRates(
@Param("errorRangeMinute") int errorRangeMinute,
@Param("delayRangeMinute") int delayRangeMinute
@@ -99,11 +99,14 @@ public class ApiStatusService {
/**
* 오류율 0% 초과 API 목록을 debug 로그로 남긴다. 개발 모드(-Deai.systemmode=D) 에서만 동작한다.
* 판정 구간에 거래가 있는 API 의 오류율·지연 판정값을 debug 로그로 남긴다.
* 개발 모드(-Deai.systemmode=D) 에서만 동작한다.
*
* <p>상태 전이 조건(정상/지연 → 장애)을 못 넘긴 API 는 {@code findApiStatusEvents} 결과에
* 나오지 않아 "오류는 나는데 장애로 안 잡힌다" 를 로그로 확인할 수 없다. 임계값·현재 상태와
* 무관하게 집계된 오류율을 그대로 찍어 판정 결과와 대조할 수 있게 한다.</p>
* <p>상태 전이 조건을 못 넘긴 API 는 {@code findApiStatusEvents} 결과에 나오지 않아
* "오류는 나는데 장애로 안 잡힌다", "느린데 지연이 아니다" 를 로그로 확인할 수 없다.
* 임계값·현재 상태와 무관하게 집계을 그대로 찍어 판정 결과와 대조할 수 있게 한다.</p>
*
* <p>오류 0건이어도 지연 판정값은 봐야 하므로 거래만 있으면 무조건 찍는다.</p>
*
* <p>진단 목적이므로 조회 실패가 상태 판정 자체를 막아서는 안 된다 - 예외는 삼킨다.</p>
*/
@@ -115,12 +118,13 @@ public class ApiStatusService {
try {
List<ApiErrorRateRow> rows = apiStatusRepository.findApiErrorRates(errorRangeMinute, delayRangeMinute);
if (rows.isEmpty()) {
log.debug("오류율 탐지: 최근 {}분간 오류 발생 API 없음", errorRangeMinute);
log.debug("상태 판정 진단: 최근 {}분(오류)/{}분(지연) 거래 발생 API 없음",
errorRangeMinute, delayRangeMinute);
return;
}
StringBuilder sb = new StringBuilder();
sb.append("오류율 탐지: 장애 임계 ").append(errorRate).append("% 초과(최근 ").append(errorRangeMinute)
sb.append("상태 판정 진단: 장애 임계 ").append(errorRate).append("% 초과(최근 ").append(errorRangeMinute)
.append("분), 지연 임계 ").append(delayAvgRespTime).append("ms 초과(최근 ").append(delayRangeMinute)
.append("분), 대상 ").append(rows.size()).append("");
@@ -139,7 +143,7 @@ public class ApiStatusService {
log.debug(sb.toString());
} catch (Exception e) {
log.debug("오류율 탐지 로그 생성 실패", e);
log.debug("상태 판정 진단 로그 생성 실패", e);
}
}