- ApiErrorRateRow 인터페이스 및 쿼리 메서드 추가 - 오류율 진단 로그 로직 구현 및 개발 모드 조건 처리 - 로그 설정(DEBUG) 변경 및 ApiStatusRepository 쿼리 개선
This commit is contained in:
@@ -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% 면 임계값과 같은 값이고, 판정은 '초과'(>) 라 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"; //점검
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user