From 98a0366be0637219368b2c8f91834a5092265961 Mon Sep 17 00:00:00 2001 From: daekuk Date: Mon, 12 Jan 2026 17:19:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?-=20=ED=86=B5=EA=B3=84=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ext/kjb/statistics/통계테이블_설계.md | 106 ++++++++++++++++-- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/eactive/ext/kjb/statistics/통계테이블_설계.md b/src/main/java/com/eactive/ext/kjb/statistics/통계테이블_설계.md index 3fb40b5..671acd5 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/통계테이블_설계.md +++ b/src/main/java/com/eactive/ext/kjb/statistics/통계테이블_설계.md @@ -4,7 +4,7 @@ ### 수집 차원 - **시간 단위**: 분 단위 -- **분류 차원**: API명, API Gateway Instance, ClientId, Scope, 업무구분코드 (값이 없을 경우 'NONE' 으로 대체) +- **분류 차원**: API명, API Gateway Instance, ClientId, Inbound Adapter, Outbound Adapter, 업무구분코드 (값이 없을 경우 'NONE' 으로 대체) ### 메트릭 - **처리 건수**: @@ -24,6 +24,23 @@ --- +## 구현 완료 + +### 구현 위치 +- **패키지**: `com.eactive.eai.custom.statistics` +- **경로**: `eapim-online/src/main/java/com/eactive/eai/custom/statistics/` + +### 주요 컴포넌트 +1. **ApiStatsCollector**: 거래마다 호출되어 메모리에 집계 +2. **MinuteStatsAggregator**: HdrHistogram 기반 백분위 계산 +3. **ApiStatsScheduler**: 매 분 5초에 DB 저장 +4. **TransactionEvent**: 거래 이벤트 DTO + +### 사용법 +상세한 사용법은 `eapim-online/src/main/java/com/eactive/eai/custom/statistics/README.md` 참조 + +--- + ## 테이블 DDL ```sql @@ -32,9 +49,10 @@ CREATE TABLE API_STATS_MINUTE ( STAT_TIME TIMESTAMP(0) NOT NULL, -- 통계 시간 (분 단위, 초는 00) API_NAME VARCHAR2(100) NOT NULL, -- API명 GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID - CLIENT_ID VARCHAR2(100) NOT NULL, -- 클라이언트 ID - SCOPE VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 스코프 BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드 + CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID + INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter + OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter -- 건수 메트릭 TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL, @@ -43,6 +61,11 @@ CREATE TABLE API_STATS_MINUTE ( SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + -- 로그 시퀀스 900 구간 에러 카운트 + SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + -- 응답시간 메트릭 (ms, 소수점 3자리) AVG_RESP_TIME NUMBER(10,3) DEFAULT 0, MIN_RESP_TIME NUMBER(10,3) DEFAULT 0, @@ -50,12 +73,8 @@ CREATE TABLE API_STATS_MINUTE ( P50_RESP_TIME NUMBER(10,3) DEFAULT 0, P95_RESP_TIME NUMBER(10,3) DEFAULT 0, - -- 관리 정보 - REG_DTIME TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, - UPD_DTIME TIMESTAMP DEFAULT SYSTIMESTAMP, - CONSTRAINT PK_API_STATS_MINUTE PRIMARY KEY ( - STAT_TIME, API_NAME, GW_INSTANCE_ID, CLIENT_ID, SCOPE, BIZ_DIV_CODE + STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER ) ); @@ -64,14 +83,18 @@ COMMENT ON TABLE API_STATS_MINUTE IS 'API Gateway 분단위 처리 통계'; COMMENT ON COLUMN API_STATS_MINUTE.STAT_TIME IS '통계시간(분단위, 초는 00초로 절사)'; COMMENT ON COLUMN API_STATS_MINUTE.API_NAME IS 'API 명칭'; COMMENT ON COLUMN API_STATS_MINUTE.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID'; -COMMENT ON COLUMN API_STATS_MINUTE.CLIENT_ID IS '클라이언트 ID'; -COMMENT ON COLUMN API_STATS_MINUTE.SCOPE IS '스코프 (없으면 NONE)'; COMMENT ON COLUMN API_STATS_MINUTE.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MINUTE.CLIENT_ID IS '클라이언트 ID (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MINUTE.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MINUTE.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)'; COMMENT ON COLUMN API_STATS_MINUTE.TOTAL_CNT IS '총 처리 건수'; COMMENT ON COLUMN API_STATS_MINUTE.SUCCESS_CNT IS '성공 건수'; COMMENT ON COLUMN API_STATS_MINUTE.TIMEOUT_CNT IS 'Timeout 오류 건수'; COMMENT ON COLUMN API_STATS_MINUTE.SYSTEM_ERR_CNT IS '시스템 오류 건수'; COMMENT ON COLUMN API_STATS_MINUTE.BIZ_ERR_CNT IS '업무 오류 건수'; +COMMENT ON COLUMN API_STATS_MINUTE.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_MINUTE.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_MINUTE.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수'; COMMENT ON COLUMN API_STATS_MINUTE.AVG_RESP_TIME IS '평균 응답시간(ms)'; COMMENT ON COLUMN API_STATS_MINUTE.MIN_RESP_TIME IS '최소 응답시간(ms)'; COMMENT ON COLUMN API_STATS_MINUTE.MAX_RESP_TIME IS '최대 응답시간(ms)'; @@ -81,3 +104,66 @@ COMMENT ON COLUMN API_STATS_MINUTE.P95_RESP_TIME IS '95 백분위 응답시간(m --- +## 시간별 통계 테이블 DDL + +```sql +-- API Gateway 시간별 통계 테이블 +CREATE TABLE API_STATS_HOUR ( + STAT_TIME TIMESTAMP(0) NOT NULL, -- 통계 시간 (시간 단위, 분/초는 00) + API_NAME VARCHAR2(100) NOT NULL, -- API명 + GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID + BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드 + CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID + INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter + OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter + + -- 건수 메트릭 + TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL, + TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 로그 시퀀스 900 구간 에러 카운트 + SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 응답시간 메트릭 (ms, 소수점 3자리) + AVG_RESP_TIME NUMBER(10,3) DEFAULT 0, + MIN_RESP_TIME NUMBER(10,3) DEFAULT 0, + MAX_RESP_TIME NUMBER(10,3) DEFAULT 0, + P50_RESP_TIME NUMBER(10,3) DEFAULT 0, + P95_RESP_TIME NUMBER(10,3) DEFAULT 0, + + CONSTRAINT PK_API_STATS_HOUR PRIMARY KEY ( + STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER + ) +); + +-- 코멘트 +COMMENT ON TABLE API_STATS_HOUR IS 'API Gateway 시간별 처리 통계'; +COMMENT ON COLUMN API_STATS_HOUR.STAT_TIME IS '통계시간(시간단위, 분/초는 00으로 절사)'; +COMMENT ON COLUMN API_STATS_HOUR.API_NAME IS 'API 명칭'; +COMMENT ON COLUMN API_STATS_HOUR.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID'; +COMMENT ON COLUMN API_STATS_HOUR.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_HOUR.CLIENT_ID IS '클라이언트 ID (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_HOUR.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_HOUR.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_HOUR.TOTAL_CNT IS '총 처리 건수'; +COMMENT ON COLUMN API_STATS_HOUR.SUCCESS_CNT IS '성공 건수'; +COMMENT ON COLUMN API_STATS_HOUR.TIMEOUT_CNT IS 'Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_HOUR.SYSTEM_ERR_CNT IS '시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_HOUR.BIZ_ERR_CNT IS '업무 오류 건수'; +COMMENT ON COLUMN API_STATS_HOUR.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_HOUR.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_HOUR.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수'; +COMMENT ON COLUMN API_STATS_HOUR.AVG_RESP_TIME IS '평균 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_HOUR.MIN_RESP_TIME IS '최소 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_HOUR.MAX_RESP_TIME IS '최대 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_HOUR.P50_RESP_TIME IS '50 백분위 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_HOUR.P95_RESP_TIME IS '95 백분위 응답시간(ms)'; +``` + +--- + From 038c99de0bc692374684e1dd8165790b7023f814 Mon Sep 17 00:00:00 2001 From: daekuk Date: Tue, 13 Jan 2026 10:36:22 +0900 Subject: [PATCH 2/4] =?UTF-8?q?-=20=EC=BF=BC=EB=A6=AC=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=EA=B8=B0=EB=8A=A5=20=EB=B9=84?= =?UTF-8?q?=EC=A0=95=EC=83=81=20=EB=8F=99=EC=9E=91,=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=20=EC=BB=A4=EB=B0=8B=EC=9C=BC=EB=A1=9C=20=EC=9B=90=EB=B3=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onl/logger/EAILogRepositoryImpl.java | 68 ++++++------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java index 5c11fce..aad5b34 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java @@ -8,6 +8,12 @@ import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + import com.eactive.eai.data.entity.onl.logger.EAILog; import com.eactive.eai.data.entity.onl.logger.EAILogId; import com.eactive.eai.data.entity.onl.logger.QEAILog; @@ -20,12 +26,6 @@ import com.querydsl.core.types.dsl.EntityPathBase; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - @Repository public class EAILogRepositoryImpl implements EAILogRepository { @@ -43,43 +43,26 @@ public class EAILogRepositoryImpl implements EAILogRepository { EntityPathBase entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName()); QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity; - - // Count Query - JPAQuery countQuery = new JPAQueryFactory(em) - .from(entityPathBase) - .leftJoin(qeaiMessageEntity) - .on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname)); - - appendConditions(eaiLogSearch, qeaiLog, entityPathBase, qeaiMessageEntity, countQuery); - long totalCount = countQuery.select(qeaiLog.id.msgdpstyms.count()).fetchOne(); + JPAQuery query = new JPAQueryFactory(em).from(entityPathBase); + appendConditions(eaiLogSearch, qeaiLog, entityPathBase, query); + + long totalCount = query.select(qeaiLog.id.msgdpstyms.count()).fetchOne(); if (totalCount == 0) { return new PageImpl<>(new ArrayList<>(), pageable, 0); } - - // List Query 분리 - JPAQuery listQuery = new JPAQueryFactory(em) - .from(entityPathBase) - .leftJoin(qeaiMessageEntity) - .on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname)); - List list = listQuery + List list = query // .select(Projections.constructor(EAILogDto.class, qeaiLog, qeaiMessageEntity.eaisvcdesc)) .select(Projections.constructor(EAILogDto.class, - qeaiLog.id, - qeaiLog.trackasiskey1ctnt, - qeaiLog.trackasiskey2ctnt, - qeaiLog.keymgtmsgctnt, - qeaiLog.eaisvcname, - qeaiLog.extbizcd, - qeaiLog.refkey, - qeaiLog.clientname, - qeaiLog.orgname, - qeaiLog.msgprcssyms, - qeaiLog.companycode, - qeaiLog.eaierrctnt, + qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt, + qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname, + qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt, qeaiMessageEntity.eaisvcdesc)) + .from(entityPathBase) + .leftJoin(qeaiMessageEntity) + .on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname)) .orderBy(qeaiLog.id.msgdpstyms.desc(), qeaiLog.id.eaisvcserno.asc(), qeaiLog.id.logprcssserno.asc()) .limit(1000) .fetch(); @@ -90,15 +73,9 @@ public class EAILogRepositoryImpl implements EAILogRepository { public Map countDetailedTransactions(Class clazz, EAILogSearch eaiLogSearch) { QEAILog qeaiLog = QEAILog.eAILog; EntityPathBase entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName()); - - QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity; - JPAQuery query = new JPAQueryFactory(em) - .from(entityPathBase) - .leftJoin(qeaiMessageEntity) - .on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eAIMessageEntity.eaisvcname)); - - appendConditions(eaiLogSearch, qeaiLog, entityPathBase, qeaiMessageEntity, query); + JPAQuery query = new JPAQueryFactory(em).from(entityPathBase); + appendConditions(eaiLogSearch, qeaiLog, entityPathBase, query); List results = query.select( qeaiLog.id.logprcssserno, // 로그 처리 상태 @@ -114,7 +91,8 @@ public class EAILogRepositoryImpl implements EAILogRepository { return reult; } - private void appendConditions(EAILogSearch eaiLogSearch, QEAILog qeaiLog, EntityPathBase entityPathBase, QEAIMessageEntity qeaiMessageEntity, JPAQuery query) { + private void appendConditions(EAILogSearch eaiLogSearch, QEAILog qeaiLog, EntityPathBase entityPathBase, JPAQuery query) { + if (StringUtils.equalsIgnoreCase(eaiLogSearch.getAuthChk(), "Y")) { QUserBusiness qUserBusiness = QUserBusiness.userBusiness; JPAQuery subQuery = new JPAQueryFactory(em) @@ -149,10 +127,6 @@ public class EAILogRepositoryImpl implements EAILogRepository { if (StringUtils.isNotBlank(eaiLogSearch.getSearchEaiSvcName())) { query.where(qeaiLog.eaisvcname.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcName())); } - - if (StringUtils.isNotBlank(eaiLogSearch.getSearchEaiSvcDesc())) { - query.where(qeaiMessageEntity.eaisvcdesc.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcDesc())); - } if (StringUtils.isNotBlank(eaiLogSearch.getSearchKeyMgtMsgCtnt())) { query.where(qeaiLog.keymgtmsgctnt.contains(eaiLogSearch.getSearchKeyMgtMsgCtnt())); From 7bc4d4e2a6891e2207526343a9dfd6d9cf74310e Mon Sep 17 00:00:00 2001 From: daekuk Date: Tue, 13 Jan 2026 13:16:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?-=20=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/jsp/onl/transaction/tracking/trackingManDetail.jsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebContent/jsp/onl/transaction/tracking/trackingManDetail.jsp b/WebContent/jsp/onl/transaction/tracking/trackingManDetail.jsp index 0951b54..ca13dc6 100644 --- a/WebContent/jsp/onl/transaction/tracking/trackingManDetail.jsp +++ b/WebContent/jsp/onl/transaction/tracking/trackingManDetail.jsp @@ -201,7 +201,7 @@ const isOutboundWithoutNet = !json.PSVSYSADPTRBZWKGROUPNAME.includes('NET'); fullMessageData = json.FULLMESSAGEDATA; layoutInfo = json.standardHeader; - bzwkdatatntInfo = json.BZWKDATATNT; + bzwkdatatntInfo = json.BZWKDATACTNT; if((isInboundPart && isInboundWithoutNet) || (isOutboundPart && isOutboundWithoutNet)){ selectHttpLog(url, prcsDate, json.EAISVCSERNO.trim(), logPrcssSerno); From ae2d9a3f08062023bcfe8cc637bf195bc51da855 Mon Sep 17 00:00:00 2001 From: daekuk Date: Tue, 13 Jan 2026 13:27:11 +0900 Subject: [PATCH 4/4] =?UTF-8?q?-=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20sort=20=EA=B8=B0=EB=8A=A5=20=EB=B9=84=EC=A0=95=EC=83=81=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/jsp/onl/admin/authserver/clientManDetail.jsp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebContent/jsp/onl/admin/authserver/clientManDetail.jsp b/WebContent/jsp/onl/admin/authserver/clientManDetail.jsp index 33dd4d8..38aaca6 100644 --- a/WebContent/jsp/onl/admin/authserver/clientManDetail.jsp +++ b/WebContent/jsp/onl/admin/authserver/clientManDetail.jsp @@ -144,9 +144,9 @@ rowNum: -1, // rowList: [10, 20, 30], // pager: '#selectedApiGridPager', - sortname: 'apiId', +// sortname: 'apiId', viewrecords: true, - sortorder: "asc", +// sortorder: "asc", caption: "선택된 API", height: 200, autowidth: true,