오류율 계산 및 로깅 로직 추가
eapim-admin CI / build (push) Has been cancelled

- ApiErrorRateRow 인터페이스 및 쿼리 메서드 추가
- 오류율 진단 로그 로직 구현 및 개발 모드 조건 처리
- 로그 설정(DEBUG) 변경 및 ApiStatusRepository 쿼리 개선
This commit is contained in:
Rinjae
2026-08-04 16:19:54 +09:00
parent cc0ef13d5d
commit 42d7fa977e
4 changed files with 186 additions and 9 deletions
@@ -0,0 +1,41 @@
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.math.BigDecimal;
/**
* 판정 구간 내 오류가 1건 이상 집계된 API 의 오류율 (개발 모드 진단 로그용)
*
* <p>{@code ApiStatusRepository.findApiErrorRates} 결과. 장애 판정 임계값과 무관하게
* 오류율 0% 초과인 API 를 모두 담는다.</p>
*/
public interface ApiErrorRateRow {
/** API(EAI 서비스)명 */
String getEaisvcname();
/** 현재 상태 코드 - 정상(N), 점검(C), 지연(D), 장애(E) */
String getStatusCode();
/** 판정 구간 내 총 건수 (성공 + 타임아웃 + 시스템오류 + 업무오류) */
BigDecimal getTotal();
/** 판정 구간 내 성공 건수 */
BigDecimal getSuccess();
/** 오류율(%) - (총건수 - 성공건수) / 총건수 * 100 */
BigDecimal getErrorRate();
/** 오류 판정 구간 내 건수 가중 평균 응답시간(ms) */
BigDecimal getAvgRespTime();
/**
* 지연 판정 구간 내 총 건수.
*
* <p>지연은 오류와 판정 구간이 다르다({@code api.status.delay.range_minute}).
* 지연 임계 비교는 이 구간의 평균 응답시간으로 해야 실제 판정과 일치한다.</p>
*/
BigDecimal getDelayTotal();
/** 지연 판정 구간 내 건수 가중 평균 응답시간(ms) */
BigDecimal getDelayAvgRespTime();
}
@@ -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<ApiStatus, String> {
*/
@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<ApiStatus, String> {
@Param("delayAvgRespTime") int delayAvgRespTime
);
/**
* 판정 구간 내 오류율 0% 초과 API 목록. 개발 모드(-Deai.systemmode=D) 진단 로그 전용.
*
* <p>{@code findApiStatusEvents} 는 임계값을 넘고 <b>상태 전이까지 일어난</b> API 만 돌려주므로
* "오류는 나는데 왜 장애로 안 잡히나" 를 볼 수 없다. 이 쿼리는 임계값·현재 상태와 무관하게
* 집계된 오류율을 그대로 보여준다.</p>
*
* <p>컬럼 alias 는 반드시 <b>따옴표로 감싼 camelCase</b> 로 둔다. 인터페이스 프로젝션은 tuple
* alias 를 getter 프로퍼티명으로 찾는데, 따옴표가 없으면 Oracle 이 alias 를 대문자로 올려
* {@code STATUS_CODE} 와 {@code statusCode} 가 매칭되지 않아 <b>조용히 null</b> 이 된다
* (단일 단어는 대소문자만 달라 우연히 매칭된다).</p>
*/
@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<ApiErrorRateRow> findApiErrorRates(
@Param("errorRangeMinute") int errorRangeMinute,
@Param("delayRangeMinute") int delayRangeMinute
);
/**
* EAI 서비스명 조회. 장애/점검 영향 API 의 API_NAME 캐시용.
*/
@@ -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<String, String> 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<ApiStatusEvent> 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<String, List<String>> eventMap = new HashMap<>();
@@ -88,6 +98,81 @@ public class ApiStatusService {
}
/**
* 오류율 0% 초과 API 목록을 debug 로그로 남긴다. 개발 모드(-Deai.systemmode=D) 에서만 동작한다.
*
* <p>상태 전이 조건(정상/지연 → 장애)을 못 넘긴 API 는 {@code findApiStatusEvents} 결과에
* 나오지 않아 "오류는 나는데 장애로 안 잡힌다" 를 로그로 확인할 수 없다. 임계값·현재 상태와
* 무관하게 집계된 오류율을 그대로 찍어 판정 결과와 대조할 수 있게 한다.</p>
*
* <p>진단 목적이므로 조회 실패가 상태 판정 자체를 막아서는 안 된다 - 예외는 삼킨다.</p>
*/
private void logErrorRates(int errorRate, int errorRangeMinute, int delayRangeMinute, int delayAvgRespTime) {
if (!isDevMode() || !log.isDebugEnabled()) {
return;
}
try {
List<ApiErrorRateRow> 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% 면 임계값과 같은 값이고, 판정은 '초과'(&gt;) 라 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();
}
/**
* 임계 초과 여부 표시. 판정 조건이 '초과'(&gt;) 이므로 임계값과 같은 값은 초과가 아니다.
*/
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"; //점검
+2 -1
View File
@@ -125,7 +125,8 @@
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
<logger name="com.eactive.eai.rms.ext.djb.job.ApiStatusMonitorJob" level="ERROR" />
<logger name="com.eactive.eai.rms.ext.djb.job" level="DEBUG" />
<logger name="com.eactive.eai.rms.ext.djb.apistatus" level="DEBUG" />
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="ERROR" additivity="false" />