From 312756adc806a162fbbdafbc23926372560fae5d Mon Sep 17 00:00:00 2001 From: pksup Date: Fri, 9 Jan 2026 10:08:12 +0900 Subject: [PATCH] =?UTF-8?q?:=20=ED=86=B5=EA=B3=84=EA=B1=B0=EB=9E=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20-=20transact?= =?UTF-8?q?ion=20=EA=B2=A9=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Url: --- WebContent/WEB-INF/applicationContext.xml | 7 +- .../statistics/ApiStatsMonitorImpl.java | 152 +++++++ .../eactive/eai/custom/statistics/README.md | 387 ++++++++++++++++++ .../collector/ApiStatsCollector.java | 152 +++++++ .../collector/MinuteStatsAggregator.java | 186 +++++++++ .../statistics/config/ApiStatsConfig.java | 95 +++++ .../custom/statistics/dto/MinuteStatsKey.java | 90 ++++ .../statistics/dto/TransactionEvent.java | 58 +++ .../eai/custom/statistics/package-info.java | 14 + .../persistence/ApiStatsMinuteLogger.java | 16 + .../persistence/ApiStatsMinuteRepository.java | 9 + .../scheduler/ApiStatsScheduler.java | 119 ++++++ .../custom/statistics/ApiStatsMinute.java | 118 ++++++ .../custom/statistics/ApiStatsMinuteId.java | 26 ++ 14 files changed, 1428 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eactive/eai/custom/statistics/ApiStatsMonitorImpl.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/README.md create mode 100644 src/main/java/com/eactive/eai/custom/statistics/collector/ApiStatsCollector.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/package-info.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java create mode 100644 src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java create mode 100644 src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java create mode 100644 src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java diff --git a/WebContent/WEB-INF/applicationContext.xml b/WebContent/WEB-INF/applicationContext.xml index cb105bc..afdc652 100644 --- a/WebContent/WEB-INF/applicationContext.xml +++ b/WebContent/WEB-INF/applicationContext.xml @@ -9,6 +9,7 @@ xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:mvc="http://www.springframework.org/schema/mvc" + xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd @@ -17,12 +18,16 @@ http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd + http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.2.xsd "> + + + - + diff --git a/src/main/java/com/eactive/eai/custom/statistics/ApiStatsMonitorImpl.java b/src/main/java/com/eactive/eai/custom/statistics/ApiStatsMonitorImpl.java new file mode 100644 index 0000000..bc9fa6e --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/ApiStatsMonitorImpl.java @@ -0,0 +1,152 @@ +package com.eactive.eai.custom.statistics; + +import org.springframework.stereotype.Component; + +import com.eactive.eai.common.message.EAIMessage; +import com.eactive.eai.common.monitor.StatisticsMonitor; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.custom.statistics.collector.ApiStatsCollector; +import com.eactive.eai.custom.statistics.dto.TransactionEvent; + +import lombok.RequiredArgsConstructor; + +/** + * StatisticsMonitor 구현체 - EAIServiceMonitor에서 호출되어 API 통계 수집 - EAIMessage에서 필요한 + * 정보를 추출하여 ApiStatsCollector에 전달 + */ +@Component +@RequiredArgsConstructor +public class ApiStatsMonitorImpl implements StatisticsMonitor { + + private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + private final ApiStatsCollector apiStatsCollector; + + @Override + public void recordStatistics(EAIMessage requestEaiMessage) { + try { + // 로그 시퀀스 400 또는 900만 처리 + int logSequence = requestEaiMessage.getLogPssSno(); + if (logSequence != 400 && logSequence != 900) { + return; + } + + String apiName = extractApiName(requestEaiMessage); + String gwInstanceId = extractGwInstanceId(requestEaiMessage); + String bizDivCode = extractBizDivCode(requestEaiMessage); + String clientId = extractClientId(requestEaiMessage); + String inboundAdapter = extractInboundAdapter(requestEaiMessage); + String outboundAdapter = extractOutboundAdapter(requestEaiMessage); + long responseTimeMs = extractResponseTime(requestEaiMessage); + boolean success = extractSuccess(requestEaiMessage); + TransactionEvent.ErrorType errorType = extractErrorType(requestEaiMessage); + + // TransactionEvent 생성 + TransactionEvent event = TransactionEvent.builder().apiName(apiName).gwInstanceId(gwInstanceId) + .bizDivCode(TransactionEvent.normalizeValue(bizDivCode)) + .clientId(TransactionEvent.normalizeValue(clientId)) + .inboundAdapter(TransactionEvent.normalizeValue(inboundAdapter)) + .outboundAdapter(TransactionEvent.normalizeValue(outboundAdapter)).responseTimeMs(responseTimeMs) + .logSequence(logSequence).success(success).errorType(errorType).build(); + + // 통계 수집 + apiStatsCollector.recordEvent(event); + + if (logger.isDebug()) { + logger.debug("API statistics recorded: " + apiName); + } + + } catch (Exception e) { + logger.error("Failed to record API statistics", e); + } + } + + /** + * API 명 추출 + */ + private String extractApiName(EAIMessage msg) { + String apiName = msg.getEAISvcCd(); + return apiName != null ? apiName : "UNKNOWN"; + } + + /** + * Gateway 인스턴스 ID 추출 + */ + private String extractGwInstanceId(EAIMessage msg) { + String instanceId = msg.getEAISvrInstNm(); + return instanceId != null ? instanceId : "UNKNOWN"; + } + + /** + * 업무구분코드 추출 + */ + private String extractBizDivCode(EAIMessage msg) { + return msg.getBwkCls(); + } + + /** + * 클라이언트 ID 추출 + */ + private String extractClientId(EAIMessage msg) { + return msg.getClientId(); + } + + /** + * Inbound Adapter 추출 + */ + private String extractInboundAdapter(EAIMessage msg) { + return msg.getSngSysItfTp(); + } + + /** + * Outbound Adapter 추출 + */ + private String extractOutboundAdapter(EAIMessage msg) { + if (msg.getCurrentSvcMsg() != null) { + // TODO 실제 값이 맞는 지 확인 필요 + return msg.getCurrentSvcMsg().getPsvSysItfTp(); + } + return null; + } + + /** + * 응답시간 추출 (ms) + */ + private long extractResponseTime(EAIMessage msg) { + long msgPssTm = msg.getMsgPssTm(); // 메시지 처리시각 + long msgRcvTm = msg.getMsgRcvTm(); // 메시지 수신시각 + + if (msgPssTm > 0 && msgRcvTm > 0) { + return msgPssTm - msgRcvTm; + } + + return 0L; + } + + /** + * 성공 여부 추출 + */ + private boolean extractSuccess(EAIMessage msg) { + return msg.getStatErrorCode() == 0; + } + + /** + * 에러 유형 추출 + */ + private TransactionEvent.ErrorType extractErrorType(EAIMessage msg) { + int errorCode = msg.getStatErrorCode(); + + switch (errorCode) { + case 0: + return null; // 정상 + case 2: + return TransactionEvent.ErrorType.TIMEOUT; + case 3: + return TransactionEvent.ErrorType.SYSTEM_ERROR; + case 1: + return TransactionEvent.ErrorType.BUSINESS_ERROR; + default: + return null; + } + } +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/README.md b/src/main/java/com/eactive/eai/custom/statistics/README.md new file mode 100644 index 0000000..e5dd844 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/README.md @@ -0,0 +1,387 @@ +# API Gateway 통계 수집 모듈 + +분단위로 API 거래 통계를 수집하고 DB에 저장하는 모듈입니다. + +## 아키텍처 + +``` +거래 발생 + ↓ +TransactionEvent 생성 + ↓ +ApiStatsCollector.recordEvent() ← 거래마다 호출 + ↓ +MinuteStatsAggregator (메모리 집계) + ↓ +ApiStatsScheduler (매 분 5초 실행) + ↓ +DB 저장 (API_STATS_MINUTE 테이블) +``` + +## 패키지 구조 + +``` +com.eactive.eai.custom.statistics/ +├── dto/ +│ ├── TransactionEvent.java - 거래 이벤트 DTO +│ └── MinuteStatsKey.java - 집계 키 (시간 + 차원) +├── collector/ +│ ├── MinuteStatsAggregator.java - HdrHistogram 기반 집계 +│ └── ApiStatsCollector.java - 메인 수집기 (ConcurrentHashMap) +├── scheduler/ +│ └── ApiStatsScheduler.java - 스케줄러 (매 분 실행) +├── ApiStatsMinuteRepository.java - JPA Repository +└── ApiStatsMinuteLogger.java - DB 저장 Logger +``` + +## 사용법 + +### 1. 거래 처리 시 통계 수집 + +거래가 발생할 때마다 `ApiStatsCollector.recordEvent()` 호출: + +```java +@Service +public class ApiGatewayService { + + @Autowired + private ApiStatsCollector statsCollector; + + public void processTransaction(ApiRequest request) { + long startTime = System.currentTimeMillis(); + + try { + // 거래 처리 + ApiResponse response = executeApi(request); + + // 통계 수집 (성공) + recordStats(request, startTime, true, null); + + return response; + + } catch (TimeoutException e) { + // 통계 수집 (Timeout) + recordStats(request, startTime, false, ErrorType.TIMEOUT); + throw e; + + } catch (SystemException e) { + // 통계 수집 (System Error) + recordStats(request, startTime, false, ErrorType.SYSTEM_ERROR); + throw e; + + } catch (BusinessException e) { + // 통계 수집 (Business Error) + recordStats(request, startTime, false, ErrorType.BUSINESS_ERROR); + throw e; + } + } + + private void recordStats(ApiRequest request, long startTime, + boolean success, ErrorType errorType) { + long responseTime = System.currentTimeMillis() - startTime; + + TransactionEvent event = TransactionEvent.builder() + .apiName(request.getApiName()) + .gwInstanceId(getInstanceId()) + .bizDivCode(TransactionEvent.normalizeValue(request.getBizDivCode())) + .clientId(TransactionEvent.normalizeValue(request.getClientId())) + .inboundAdapter(TransactionEvent.normalizeValue(request.getInboundAdapter())) + .outboundAdapter(TransactionEvent.normalizeValue(request.getOutboundAdapter())) + .responseTimeMs(responseTime) + .success(success) + .errorType(errorType) + .build(); + + statsCollector.recordEvent(event); + } +} +``` + +### 2. 스케줄러 자동 실행 + +`ApiStatsScheduler`가 자동으로 매 분 5초에 실행되어 DB에 저장합니다. + +``` +매 분 5초 (예: 10:00:05, 10:01:05, ...) + ↓ +완료된 분의 통계를 flush (예: 10:00:00 분 데이터) + ↓ +DB 저장 +``` + +### 3. 스케줄러 활성화 + +Spring Boot 애플리케이션에 `@EnableScheduling` 추가: + +```java +@SpringBootApplication +@EnableScheduling // ← 스케줄러 활성화 +public class EapimOnlineApplication { + public static void main(String[] args) { + SpringApplication.run(EapimOnlineApplication.class, args); + } +} +``` + +## 주요 클래스 설명 + +### TransactionEvent + +거래 1건의 정보를 담는 DTO: +- API명, Instance ID, 업무구분코드, Client ID +- Inbound/Outbound Adapter +- 응답시간, 성공 여부, 오류 유형 + +### MinuteStatsAggregator + +분단위 집계를 수행: +- **HdrHistogram**: P50, P95 백분위 계산 +- **AtomicLong**: 건수 카운터 (Thread-safe) +- **snapshot()**: 집계 결과를 Entity로 변환 + +### ApiStatsCollector + +메모리에서 통계 수집: +- **ConcurrentHashMap**: 멀티스레드 안전 +- **computeIfAbsent()**: Lock-free 집계 +- **flushCompletedMinutes()**: 완료된 분의 데이터 반환 + +### ApiStatsScheduler + +매 분마다 DB 저장: +- **@Scheduled(cron = "5 * * * * ?")**: 매 분 5초 실행 +- **@PreDestroy**: 종료 시 남은 데이터 저장 + +## 성능 특징 + +### 메모리 효율 + +- **HdrHistogram**: 수만 건을 2~4KB로 압축 +- **ConcurrentHashMap**: 차원별로 독립적인 집계 +- **분 단위 flush**: 메모리 누적 방지 + +### Thread-safe + +- **ConcurrentHashMap**: Lock-free read +- **AtomicLong**: Lock-free increment +- **synchronized**: HdrHistogram 기록 시만 사용 + +### 성능 영향 최소화 + +- 거래당 오버헤드: < 1ms (메모리 연산만) +- DB 저장: 비동기 (스케줄러) +- Lock contention: 최소화 + +## 설정 + +### PropManager를 통한 동적 설정 (권장) + +운영 중에 변경 가능한 설정값을 PropManager로 관리합니다. + +#### 1. DB에 설정값 등록 + +PropManager 그룹명: **`ApiStats`** + +| 키 | 설명 | 기본값 | 예시 | +|---|---|---|---| +| `enabled` | 통계 수집 활성화 여부 | true | true: 활성화, false: 비활성화 | +| `histogram.min.ms` | 히스토그램 최소값 (ms) | 1 | 응답시간 최소 기록값 | +| `histogram.max.ms` | 히스토그램 최대값 (ms) | 60000 | 60초 초과 시 클리핑 | +| `histogram.precision` | 히스토그램 정밀도 (유효숫자) | 3 | 3자리 정밀도 | + +**DB 설정 예시:** +```sql +-- TSEAIRM24 테이블에 설정값 추가 +INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION) +VALUES ('ApiStats', 'enabled', 'true', '통계 수집 활성화 여부'); + +INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION) +VALUES ('ApiStats', 'histogram.min.ms', '1', '히스토그램 최소값(ms)'); + +INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION) +VALUES ('ApiStats', 'histogram.max.ms', '60000', '히스토그램 최대값(ms)'); + +INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION) +VALUES ('ApiStats', 'histogram.precision', '3', '히스토그램 정밀도'); +``` + +#### 2. 설정 변경 방법 + +**통계 수집 OFF (긴급 상황 시)** +```sql +-- 통계 수집 비활성화 +UPDATE TSEAIRM24 +SET PROP_VALUE = 'false' +WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled'; +``` + +```java +// PropManager 리로드 (즉시 적용) +PropManager.getInstance().reload("ApiStats"); +``` + +**통계 수집 ON (다시 활성화)** +```sql +-- 통계 수집 재활성화 +UPDATE TSEAIRM24 +SET PROP_VALUE = 'true' +WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled'; +``` + +```java +// PropManager 리로드 +PropManager.getInstance().reload("ApiStats"); +``` + +**히스토그램 범위 변경** +```sql +-- 최대값을 300초로 변경 +UPDATE TSEAIRM24 +SET PROP_VALUE = '300000' +WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'histogram.max.ms'; +``` + +```java +// PropManager 리로드 +PropManager.getInstance().reload("ApiStats"); +``` + +**방법 2: 관리 화면을 통한 수정** +- EMS 관리 화면에서 PropManager 설정 수정 +- 저장 시 자동으로 메모리에 반영 + +#### 3. 설정값 확인 + +애플리케이션 시작 시 로그에서 확인: +``` +ApiStatsScheduler initialized +ApiStatsConfig] enabled = true +ApiStatsConfig] histogram.min.ms = 1 +ApiStatsConfig] histogram.max.ms = 60000 +ApiStatsConfig] histogram.precision = 3 +``` + +**비활성화 시 로그:** +``` +ApiStatsConfig] enabled = false +API stats collection is disabled, skipping save +``` + +### 스케줄러 주기 변경 + +`ApiStatsScheduler.java`의 cron 표현식 수정: + +```java +@Scheduled(cron = "5 * * * * ?") // 매 분 5초 +@Scheduled(cron = "0 * * * * ?") // 매 분 0초 +@Scheduled(cron = "0 */5 * * * ?") // 5분마다 +``` + +**주의:** 스케줄러 주기는 코드 변경 후 재배포 필요 + +## 모니터링 + +### 활성 집계기 개수 확인 + +```java +int count = statsCollector.getActiveAggregatorCount(); +log.info("Active aggregators: {}", count); +``` + +### 로그 레벨 조정 + +```properties +# application.properties +logging.level.com.eactive.eai.custom.statistics=DEBUG +``` + +## 주의사항 + +1. **NULL 값 처리**: 차원 값이 NULL이면 "NONE"으로 변환 필수 +2. **응답시간 범위**: PropManager 설정값 범위 초과 시 클리핑됨 (기본: 1ms ~ 60초) +3. **스케줄러 활성화**: `@EnableScheduling` 필수 +4. **DB 저장 실패**: 로그로 기록되며 다음 분에 재시도하지 않음 +5. **PropManager 설정**: `ApiStats` 그룹이 DB에 없으면 기본값 사용 + +## 테스트 + +### 단위 테스트 예제 + +```java +@Test +public void testStatsCollection() { + ApiStatsCollector collector = new ApiStatsCollector(); + + // 거래 이벤트 생성 + TransactionEvent event = TransactionEvent.builder() + .apiName("TEST_API") + .gwInstanceId("GW01") + .bizDivCode("NONE") + .clientId("CLIENT01") + .inboundAdapter("HTTP") + .outboundAdapter("REST") + .responseTimeMs(100) + .success(true) + .build(); + + // 이벤트 기록 + LocalDateTime now = LocalDateTime.of(2026, 1, 8, 10, 0, 30); + collector.recordEvent(event, now); + + // Flush (10시 1분 이전) + LocalDateTime flushTime = LocalDateTime.of(2026, 1, 8, 10, 1, 0); + List result = collector.flushBefore(flushTime); + + // 검증 + assertEquals(1, result.size()); + ApiStatsMinute stats = result.get(0); + assertEquals("TEST_API", stats.getApiName()); + assertEquals(1L, stats.getTotalCnt()); + assertEquals(1L, stats.getSuccessCnt()); +} +``` + +## 문제 해결 + +### 통계가 저장되지 않음 + +1. `@EnableScheduling` 확인 +2. 로그에서 스케줄러 실행 확인 +3. DB 연결 및 권한 확인 + +### 메모리 증가 + +1. 활성 집계기 개수 확인 +2. 스케줄러 정상 동작 확인 +3. 차원 카디널리티 확인 (너무 많은 고유 키) + +### P50, P95 값이 이상함 + +1. 응답시간 단위 확인 (ms) +2. PropManager 설정값 확인 (`ApiStats` 그룹) +3. 히스토그램 범위 확인 (기본: 1~60000ms) +4. 데이터 건수 확인 (최소 1건 이상) + +### PropManager 설정이 적용되지 않음 + +1. DB에 `ApiStats` 그룹 설정 확인 +2. PropManager 초기화 확인 +3. 로그에서 설정값 로딩 확인 +4. 설정 변경 후 `PropManager.reload("ApiStats")` 실행 + +### 통계 수집을 비활성화하고 싶음 + +**운영 중 성능 이슈 발생 시:** +```sql +-- DB에서 비활성화 +UPDATE TSEAIRM24 SET PROP_VALUE = 'false' +WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled'; +``` + +```java +// 즉시 적용 +PropManager.getInstance().reload("ApiStats"); +``` + +이후 모든 `recordEvent()` 호출과 스케줄러 실행이 즉시 중단됩니다. +메모리에 쌓인 데이터는 유지되며, 다시 활성화 시 계속 수집됩니다. diff --git a/src/main/java/com/eactive/eai/custom/statistics/collector/ApiStatsCollector.java b/src/main/java/com/eactive/eai/custom/statistics/collector/ApiStatsCollector.java new file mode 100644 index 0000000..c10a904 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/collector/ApiStatsCollector.java @@ -0,0 +1,152 @@ +package com.eactive.eai.custom.statistics.collector; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Service; + +import com.eactive.eai.custom.statistics.config.ApiStatsConfig; +import com.eactive.eai.custom.statistics.dto.MinuteStatsKey; +import com.eactive.eai.custom.statistics.dto.TransactionEvent; +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute; + +import lombok.extern.slf4j.Slf4j; + +/** + * API 통계 수집기 + * - 거래 이벤트를 분단위로 메모리에 집계 + * - Thread-safe한 ConcurrentHashMap 사용 + */ +@Slf4j +@Service +public class ApiStatsCollector { + + /** + * 분단위 집계 맵 + * Key: MinuteStatsKey (시간 + 차원들) + * Value: MinuteStatsAggregator (집계 데이터) + */ + private final ConcurrentHashMap aggregators = new ConcurrentHashMap<>(); + + /** + * 거래 이벤트 기록 + * + * @param event 거래 이벤트 + */ + public void recordEvent(TransactionEvent event) { + recordEvent(event, LocalDateTime.now()); + } + + /** + * 거래 이벤트 기록 (시간 지정 - 테스트용) + * + * @param event 거래 이벤트 + * @param eventTime 이벤트 발생 시간 + */ + public void recordEvent(TransactionEvent event, LocalDateTime eventTime) { + // 통계 수집 비활성화 체크 + if (!ApiStatsConfig.isEnabled()) { + if (log.isDebugEnabled()) { + log.debug("API stats collection is disabled"); + } + return; + } + + try { + // 집계 키 생성 (분 단위로 절사) + MinuteStatsKey key = MinuteStatsKey.from(event, eventTime); + + // 집계기 가져오기 또는 생성 + MinuteStatsAggregator aggregator = aggregators.computeIfAbsent( + key, + MinuteStatsAggregator::new + ); + + // 이벤트 기록 + aggregator.recordEvent(event); + + if (log.isDebugEnabled()) { + log.debug("Recorded event: {}, total count: {}", + key, aggregator.getTotalCount()); + } + + } catch (Exception e) { + log.error("Failed to record event: {}", event, e); + } + } + + /** + * 지정된 시간 이전의 집계 데이터를 flush하여 반환 + * - 반환된 데이터는 메모리에서 제거됨 + * + * @param beforeTime 이 시간 이전의 데이터를 flush (exclusive) + * @return 집계된 통계 리스트 + */ + public List flushBefore(LocalDateTime beforeTime) { + LocalDateTime cutoffTime = beforeTime.truncatedTo(ChronoUnit.MINUTES); + List result = new ArrayList<>(); + + // 대상 키 수집 및 스냅샷 생성 + List keysToRemove = new ArrayList<>(); + + aggregators.forEach((key, aggregator) -> { + if (key.getStatTime().isBefore(cutoffTime)) { + // 스냅샷 생성 (건수가 0보다 큰 경우만) + if (aggregator.getTotalCount() > 0) { + ApiStatsMinute snapshot = aggregator.snapshot(); + result.add(snapshot); + } + keysToRemove.add(key); + } + }); + + // 메모리에서 제거 + keysToRemove.forEach(aggregators::remove); + + log.info("Flushed {} stats records before {}, remaining: {}", + result.size(), cutoffTime, aggregators.size()); + + return result; + } + + /** + * 현재 분의 집계 데이터를 flush + * - 현재 분 이전의 모든 데이터를 반환하고 제거 + */ + public List flushCompletedMinutes() { + // 현재 분은 아직 진행 중이므로 제외 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime currentMinute = now.truncatedTo(ChronoUnit.MINUTES); + + return flushBefore(currentMinute); + } + + /** + * 모든 집계 데이터를 강제로 flush (종료 시 사용) + */ + public List flushAll() { + List result = new ArrayList<>(); + + aggregators.forEach((key, aggregator) -> { + if (aggregator.getTotalCount() > 0) { + result.add(aggregator.snapshot()); + } + }); + + aggregators.clear(); + + log.info("Flushed all {} stats records", result.size()); + + return result; + } + + /** + * 현재 집계 중인 키 개수 반환 (모니터링용) + */ + public int getActiveAggregatorCount() { + return aggregators.size(); + } +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java b/src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java new file mode 100644 index 0000000..15b0e97 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java @@ -0,0 +1,186 @@ +package com.eactive.eai.custom.statistics.collector; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.concurrent.atomic.AtomicLong; + +import org.HdrHistogram.Histogram; + +import com.eactive.eai.custom.statistics.config.ApiStatsConfig; +import com.eactive.eai.custom.statistics.dto.MinuteStatsKey; +import com.eactive.eai.custom.statistics.dto.TransactionEvent; +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute; + +import lombok.Getter; + +/** + * 분단위 통계 집계기 + * - HdrHistogram을 사용하여 응답시간 백분위 계산 + * - Thread-safe한 집계 처리 + */ +public class MinuteStatsAggregator { + + @Getter + private final MinuteStatsKey key; + + // 건수 메트릭 + private final AtomicLong totalCnt = new AtomicLong(0); + private final AtomicLong successCnt = new AtomicLong(0); + private final AtomicLong timeoutCnt = new AtomicLong(0); + private final AtomicLong systemErrCnt = new AtomicLong(0); + private final AtomicLong bizErrCnt = new AtomicLong(0); + + // 로그 시퀀스 900 구간 에러 카운트 + private final AtomicLong seq900TimeoutCnt = new AtomicLong(0); + private final AtomicLong seq900SystemErrCnt = new AtomicLong(0); + private final AtomicLong seq900BizErrCnt = new AtomicLong(0); + + // 응답시간 히스토그램 (1ms ~ 60초, 3자리 정밀도) + private final Histogram histogram; + + // 응답시간 통계용 + private final AtomicLong totalRespTime = new AtomicLong(0); // 평균 계산용 + private volatile long minRespTime = Long.MAX_VALUE; + private volatile long maxRespTime = Long.MIN_VALUE; + + public MinuteStatsAggregator(MinuteStatsKey key) { + this.key = key; + // HdrHistogram: PropManager에서 설정값 읽기 + long minMs = ApiStatsConfig.getHistogramMinMs(); + long maxMs = ApiStatsConfig.getHistogramMaxMs(); + int precision = ApiStatsConfig.getHistogramPrecision(); + this.histogram = new Histogram(minMs, maxMs, precision); + } + + /** + * 거래 이벤트 기록 + */ + public synchronized void recordEvent(TransactionEvent event) { + int logSeq = event.getLogSequence(); + + // 로그 시퀀스 400: 일반 통계 집계 + if (logSeq == 400) { + totalCnt.incrementAndGet(); + + if (event.isSuccess()) { + successCnt.incrementAndGet(); + } else { + switch (event.getErrorType()) { + case TIMEOUT: + timeoutCnt.incrementAndGet(); + break; + case SYSTEM_ERROR: + systemErrCnt.incrementAndGet(); + break; + case BUSINESS_ERROR: + bizErrCnt.incrementAndGet(); + break; + } + } + + // 응답시간 기록 (400 구간만) + long respTime = event.getResponseTimeMs(); + totalRespTime.addAndGet(respTime); + + // 히스토그램에 기록 + long maxMs = ApiStatsConfig.getHistogramMaxMs(); + try { + long clippedRespTime = Math.min(Math.max(1, respTime), maxMs); + histogram.recordValue(clippedRespTime); + } catch (ArrayIndexOutOfBoundsException e) { + histogram.recordValue(maxMs); + } + + // Min/Max 업데이트 + updateMinMax(respTime); + } + // 로그 시퀀스 900: 900 전용 에러 카운트만 집계 + else if (logSeq == 900 && !event.isSuccess()) { + switch (event.getErrorType()) { + case TIMEOUT: + seq900TimeoutCnt.incrementAndGet(); + break; + case SYSTEM_ERROR: + seq900SystemErrCnt.incrementAndGet(); + break; + case BUSINESS_ERROR: + seq900BizErrCnt.incrementAndGet(); + break; + } + } + // 그 외 로그 시퀀스는 무시 + } + + /** + * Min/Max 응답시간 업데이트 (thread-safe) + */ + private synchronized void updateMinMax(long respTime) { + if (respTime < minRespTime) { + minRespTime = respTime; + } + if (respTime > maxRespTime) { + maxRespTime = respTime; + } + } + + /** + * 현재 집계 결과를 Entity로 변환 + */ + public ApiStatsMinute snapshot() { + ApiStatsMinute stats = new ApiStatsMinute(); + + // 키 정보 + stats.setStatTime(key.getStatTime()); + stats.setApiName(key.getApiName()); + stats.setGwInstanceId(key.getGwInstanceId()); + stats.setBizDivCode(key.getBizDivCode()); + stats.setClientId(key.getClientId()); + stats.setInboundAdapter(key.getInboundAdapter()); + stats.setOutboundAdapter(key.getOutboundAdapter()); + + // 건수 메트릭 + stats.setTotalCnt(totalCnt.get()); + stats.setSuccessCnt(successCnt.get()); + stats.setTimeoutCnt(timeoutCnt.get()); + stats.setSystemErrCnt(systemErrCnt.get()); + stats.setBizErrCnt(bizErrCnt.get()); + + // 로그 시퀀스 900 구간 에러 카운트 + stats.setSeq900TimeoutCnt(seq900TimeoutCnt.get()); + stats.setSeq900SystemErrCnt(seq900SystemErrCnt.get()); + stats.setSeq900BizErrCnt(seq900BizErrCnt.get()); + + // 응답시간 메트릭 + long total = totalCnt.get(); + if (total > 0) { + // 평균 + double avg = (double) totalRespTime.get() / total; + stats.setAvgRespTime(BigDecimal.valueOf(avg).setScale(3, RoundingMode.HALF_UP)); + + // Min/Max + stats.setMinRespTime(BigDecimal.valueOf(minRespTime).setScale(3, RoundingMode.HALF_UP)); + stats.setMaxRespTime(BigDecimal.valueOf(maxRespTime).setScale(3, RoundingMode.HALF_UP)); + + // 백분위 (HdrHistogram 사용) + long p50 = histogram.getValueAtPercentile(50.0); + long p95 = histogram.getValueAtPercentile(95.0); + stats.setP50RespTime(BigDecimal.valueOf(p50).setScale(3, RoundingMode.HALF_UP)); + stats.setP95RespTime(BigDecimal.valueOf(p95).setScale(3, RoundingMode.HALF_UP)); + } else { + stats.setAvgRespTime(BigDecimal.ZERO); + stats.setMinRespTime(BigDecimal.ZERO); + stats.setMaxRespTime(BigDecimal.ZERO); + stats.setP50RespTime(BigDecimal.ZERO); + stats.setP95RespTime(BigDecimal.ZERO); + } + + return stats; + } + + /** + * 집계된 총 건수 반환 (빈 집계기 체크용) + */ + public long getTotalCount() { + return totalCnt.get(); + } +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java b/src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java new file mode 100644 index 0000000..bdb7e36 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java @@ -0,0 +1,95 @@ +package com.eactive.eai.custom.statistics.config; + +import java.util.Properties; + +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.Logger; + +import lombok.experimental.UtilityClass; + +/** + * API 통계 설정 관리 + * - PropManager를 통해 운영 중 동적 변경 가능한 설정값 관리 + */ +@UtilityClass +public class ApiStatsConfig { + + private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + /** PropManager 그룹명 */ + private static final String GROUP_NAME = "ApiStats"; + + /** 기본값 상수 */ + private static final boolean DEFAULT_ENABLED = true; + private static final long DEFAULT_HISTOGRAM_MIN_MS = 1L; + private static final long DEFAULT_HISTOGRAM_MAX_MS = 60000L; + private static final int DEFAULT_HISTOGRAM_PRECISION = 3; + + /** + * 통계 수집 활성화 여부 + * - true: 통계 수집 활성화 (기본값) + * - false: 통계 수집 비활성화 + */ + public static boolean isEnabled() { + try { + Properties prop = PropManager.getInstance().getProperties(GROUP_NAME); + String value = prop.getProperty("enabled", String.valueOf(DEFAULT_ENABLED)); + return Boolean.parseBoolean(value); + } catch (Exception e) { + logger.debug("Failed to get enabled from PropManager, using default: " + DEFAULT_ENABLED); + return DEFAULT_ENABLED; + } + } + + /** + * 히스토그램 최소값 (ms) + */ + public static long getHistogramMinMs() { + try { + Properties prop = PropManager.getInstance().getProperties(GROUP_NAME); + String value = prop.getProperty("histogram.min.ms", String.valueOf(DEFAULT_HISTOGRAM_MIN_MS)); + return Long.parseLong(value); + } catch (Exception e) { + logger.debug("Failed to get histogram.min.ms from PropManager, using default: " + DEFAULT_HISTOGRAM_MIN_MS); + return DEFAULT_HISTOGRAM_MIN_MS; + } + } + + /** + * 히스토그램 최대값 (ms) + */ + public static long getHistogramMaxMs() { + try { + Properties prop = PropManager.getInstance().getProperties(GROUP_NAME); + String value = prop.getProperty("histogram.max.ms", String.valueOf(DEFAULT_HISTOGRAM_MAX_MS)); + return Long.parseLong(value); + } catch (Exception e) { + logger.debug("Failed to get histogram.max.ms from PropManager, using default: " + DEFAULT_HISTOGRAM_MAX_MS); + return DEFAULT_HISTOGRAM_MAX_MS; + } + } + + /** + * 히스토그램 정밀도 (유효숫자) + */ + public static int getHistogramPrecision() { + try { + Properties prop = PropManager.getInstance().getProperties(GROUP_NAME); + String value = prop.getProperty("histogram.precision", String.valueOf(DEFAULT_HISTOGRAM_PRECISION)); + return Integer.parseInt(value); + } catch (Exception e) { + logger.debug("Failed to get histogram.precision from PropManager, using default: " + DEFAULT_HISTOGRAM_PRECISION); + return DEFAULT_HISTOGRAM_PRECISION; + } + } + + /** + * 설정값 로깅 (초기화 시) + */ + public static void logCurrentConfig() { + logger.info("ApiStatsConfig] enabled = " + isEnabled()); + logger.info("ApiStatsConfig] histogram.min.ms = " + getHistogramMinMs()); + logger.info("ApiStatsConfig] histogram.max.ms = " + getHistogramMaxMs()); + logger.info("ApiStatsConfig] histogram.precision = " + getHistogramPrecision()); + } +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java b/src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java new file mode 100644 index 0000000..f2d6797 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java @@ -0,0 +1,90 @@ +package com.eactive.eai.custom.statistics.dto; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +import lombok.Getter; + +/** + * 분단위 통계 집계 키 + * - 동일 분의 동일 차원별로 집계하기 위한 키 + */ +@Getter +public class MinuteStatsKey { + + /** 통계 시간 (분 단위, 초는 00) */ + private final LocalDateTime statTime; + + /** API명 */ + private final String apiName; + + /** API Gateway 인스턴스 ID */ + private final String gwInstanceId; + + /** 업무구분코드 */ + private final String bizDivCode; + + /** 클라이언트 ID */ + private final String clientId; + + /** Inbound Adapter */ + private final String inboundAdapter; + + /** Outbound Adapter */ + private final String outboundAdapter; + + public MinuteStatsKey(LocalDateTime statTime, String apiName, String gwInstanceId, + String bizDivCode, String clientId, String inboundAdapter, String outboundAdapter) { + // 초와 나노초를 0으로 절사하여 분 단위로 정규화 + this.statTime = statTime.truncatedTo(ChronoUnit.MINUTES); + this.apiName = apiName; + this.gwInstanceId = gwInstanceId; + this.bizDivCode = bizDivCode; + this.clientId = clientId; + this.inboundAdapter = inboundAdapter; + this.outboundAdapter = outboundAdapter; + } + + /** + * TransactionEvent로부터 키 생성 + */ + public static MinuteStatsKey from(TransactionEvent event, LocalDateTime eventTime) { + return new MinuteStatsKey( + eventTime, + event.getApiName(), + event.getGwInstanceId(), + event.getBizDivCode(), + event.getClientId(), + event.getInboundAdapter(), + event.getOutboundAdapter() + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MinuteStatsKey that = (MinuteStatsKey) o; + return Objects.equals(statTime, that.statTime) && + Objects.equals(apiName, that.apiName) && + Objects.equals(gwInstanceId, that.gwInstanceId) && + Objects.equals(bizDivCode, that.bizDivCode) && + Objects.equals(clientId, that.clientId) && + Objects.equals(inboundAdapter, that.inboundAdapter) && + Objects.equals(outboundAdapter, that.outboundAdapter); + } + + @Override + public int hashCode() { + return Objects.hash(statTime, apiName, gwInstanceId, bizDivCode, + clientId, inboundAdapter, outboundAdapter); + } + + @Override + public String toString() { + return String.format("%s|%s|%s|%s|%s|%s|%s", + statTime, apiName, gwInstanceId, bizDivCode, + clientId, inboundAdapter, outboundAdapter); + } +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java b/src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java new file mode 100644 index 0000000..60a0793 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java @@ -0,0 +1,58 @@ +package com.eactive.eai.custom.statistics.dto; + +import lombok.Builder; +import lombok.Getter; + +/** + * 거래 1건의 이벤트 정보 + */ +@Getter +@Builder +public class TransactionEvent { + + /** API명 */ + private final String apiName; + + /** API Gateway 인스턴스 ID */ + private final String gwInstanceId; + + /** 업무구분코드 (없으면 "NONE") */ + private final String bizDivCode; + + /** 클라이언트 ID (없으면 "NONE") */ + private final String clientId; + + /** Inbound Adapter (없으면 "NONE") */ + private final String inboundAdapter; + + /** Outbound Adapter (없으면 "NONE") */ + private final String outboundAdapter; + + /** 응답시간 (ms) */ + private final long responseTimeMs; + + /** 성공 여부 */ + private final boolean success; + + /** 오류 유형 (성공시 null) */ + private final ErrorType errorType; + + /** 로그 시퀀스 번호 (100, 200, 300, 400, 900) */ + private final int logSequence; + + /** + * 오류 유형 + */ + public enum ErrorType { + TIMEOUT, // Timeout 오류 + SYSTEM_ERROR, // 시스템 오류 + BUSINESS_ERROR // 업무 오류 + } + + /** + * 기본값을 "NONE"으로 정규화 + */ + public static String normalizeValue(String value) { + return (value == null || value.trim().isEmpty()) ? "NONE" : value; + } +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/package-info.java b/src/main/java/com/eactive/eai/custom/statistics/package-info.java new file mode 100644 index 0000000..a61de7a --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/package-info.java @@ -0,0 +1,14 @@ +/** + * API Gateway 통계 수집 모듈 + * + *

분단위로 API 거래 통계를 수집하고 DB에 저장합니다. + * HdrHistogram을 사용하여 메모리 효율적으로 응답시간 백분위(P50, P95)를 계산합니다. + * + *

주요 컴포넌트

+ *
    + *
  • ApiStatsMonitorImpl - StatisticsMonitor 구현체 (진입점)
  • + *
  • ApiStatsCollector - 거래 이벤트 수집 및 메모리 집계
  • + *
  • ApiStatsScheduler - 매 분 DB 저장 스케줄러
  • + *
+ */ +package com.eactive.eai.custom.statistics; \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java new file mode 100644 index 0000000..9d79575 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java @@ -0,0 +1,16 @@ +package com.eactive.eai.custom.statistics.persistence; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute; +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinuteId; +import com.eactive.eai.data.jpa.AbstractDataLogger; + +@Service +@Transactional(propagation = Propagation.REQUIRES_NEW) +public class ApiStatsMinuteLogger + extends AbstractDataLogger { + +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java new file mode 100644 index 0000000..e114c12 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java @@ -0,0 +1,9 @@ +package com.eactive.eai.custom.statistics.persistence; + +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute; +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinuteId; +import com.eactive.eai.data.jpa.BaseRepository; + +public interface ApiStatsMinuteRepository extends BaseRepository { + +} diff --git a/src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java b/src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java new file mode 100644 index 0000000..3918e73 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java @@ -0,0 +1,119 @@ +package com.eactive.eai.custom.statistics.scheduler; + +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.eactive.eai.custom.statistics.collector.ApiStatsCollector; +import com.eactive.eai.custom.statistics.config.ApiStatsConfig; +import com.eactive.eai.custom.statistics.persistence.ApiStatsMinuteLogger; +import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * API 통계 스케줄러 + * - 매 분마다 실행되어 집계된 통계를 DB에 저장 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiStatsScheduler { + + private final ApiStatsCollector collector; + private final ApiStatsMinuteLogger logger; + + /** + * 초기화 시 설정값 로깅 + */ + @PostConstruct + public void init() { + log.info("ApiStatsScheduler initialized"); + ApiStatsConfig.logCurrentConfig(); + } + + /** + * 매 분마다 실행 (매 분 5초에 실행) + * - 이전 분의 완료된 통계를 DB에 저장 + */ + @Scheduled(cron = "5 * * * * ?") + public void saveMinuteStats() { + // 통계 수집 비활성화 체크 + if (!ApiStatsConfig.isEnabled()) { + log.debug("API stats collection is disabled, skipping save"); + return; + } + + try { + log.debug("Starting minute stats flush..."); + + // 완료된 분의 통계 데이터 가져오기 + List statsList = collector.flushCompletedMinutes(); + + if (statsList.isEmpty()) { + log.debug("No stats to save"); + return; + } + + // DB에 저장 + int savedCount = 0; + int errorCount = 0; + + for (ApiStatsMinute stats : statsList) { + try { + logger.log(stats); + savedCount++; + + if (log.isDebugEnabled()) { + log.debug("Saved stats: {} - total={}, success={}, p95={}ms", + stats.getApiName(), + stats.getTotalCnt(), + stats.getSuccessCnt(), + stats.getP95RespTime()); + } + + } catch (Exception e) { + errorCount++; + log.error("Failed to save stats: {}", stats, e); + } + } + + log.info("Minute stats saved: success={}, error={}, active_aggregators={}", + savedCount, errorCount, collector.getActiveAggregatorCount()); + + } catch (Exception e) { + log.error("Failed to flush minute stats", e); + } + } + + /** + * 애플리케이션 종료 시 남은 통계 저장 + */ + @PreDestroy + public void shutdown() { + try { + log.info("Shutting down ApiStatsScheduler, flushing remaining stats..."); + + List statsList = collector.flushAll(); + + if (!statsList.isEmpty()) { + for (ApiStatsMinute stats : statsList) { + try { + logger.log(stats); + } catch (Exception e) { + log.error("Failed to save stats during shutdown: {}", stats, e); + } + } + log.info("Saved {} remaining stats during shutdown", statsList.size()); + } + + } catch (Exception e) { + log.error("Error during shutdown", e); + } + } +} diff --git a/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java new file mode 100644 index 0000000..919e792 --- /dev/null +++ b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java @@ -0,0 +1,118 @@ +package com.eactive.eai.data.entity.custom.statistics; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; + +import org.hibernate.annotations.Comment; + +import lombok.Getter; +import lombok.Setter; + +/** + * API Gateway 분단위 처리 통계 + */ +@Getter +@Setter +@Entity +@Table(name = "API_STATS_MINUTE") +@org.hibernate.annotations.Table(appliesTo = "API_STATS_MINUTE", comment = "API Gateway 분단위 처리 통계") +@IdClass(ApiStatsMinuteId.class) +public class ApiStatsMinute implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STAT_TIME", nullable = false) + @Comment("통계시간(분단위, 초는 00초로 절사)") + private LocalDateTime statTime; + + @Id + @Column(name = "API_NAME", nullable = false, length = 100) + @Comment("API 명칭") + private String apiName; + + @Id + @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50) + @Comment("API Gateway 인스턴스 ID") + private String gwInstanceId; + + @Id + @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50, columnDefinition = "VARCHAR2(50) DEFAULT 'NONE'") + @Comment("업무구분코드 (없으면 NONE)") + private String bizDivCode = "NONE"; + + @Id + @Column(name = "CLIENT_ID", nullable = false, length = 100, columnDefinition = "VARCHAR2(100) DEFAULT 'NONE'") + @Comment("클라이언트 ID (없으면 NONE)") + private String clientId = "NONE"; + + @Id + @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100, columnDefinition = "VARCHAR2(100) DEFAULT 'NONE'") + @Comment("Inbound Adapter (없으면 NONE)") + private String inboundAdapter = "NONE"; + + @Id + @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100, columnDefinition = "VARCHAR2(100) DEFAULT 'NONE'") + @Comment("Outbound Adapter (없으면 NONE)") + private String outboundAdapter = "NONE"; + + // 건수 메트릭 + @Column(name = "TOTAL_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("총 처리 건수") + private Long totalCnt = 0L; + + @Column(name = "SUCCESS_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("성공 건수") + private Long successCnt = 0L; + + @Column(name = "TIMEOUT_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("Timeout 오류 건수") + private Long timeoutCnt = 0L; + + @Column(name = "SYSTEM_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("시스템 오류 건수") + private Long systemErrCnt = 0L; + + @Column(name = "BIZ_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("업무 오류 건수") + private Long bizErrCnt = 0L; + + @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("로그시퀀스900 Timeout 오류 건수") + private Long seq900TimeoutCnt = 0L; + + @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("로그시퀀스900 시스템 오류 건수") + private Long seq900SystemErrCnt = 0L; + + @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0") + @Comment("로그시퀀스900 업무 오류 건수") + private Long seq900BizErrCnt = 0L; + + // 응답시간 메트릭 (ms, 소수점 3자리) + @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0") + @Comment("평균 응답시간(ms)") + private BigDecimal avgRespTime = BigDecimal.ZERO; + + @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0") + @Comment("최소 응답시간(ms)") + private BigDecimal minRespTime = BigDecimal.ZERO; + + @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0") + @Comment("최대 응답시간(ms)") + private BigDecimal maxRespTime = BigDecimal.ZERO; + + @Column(name = "P50_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0") + @Comment("50 백분위 응답시간(ms)") + private BigDecimal p50RespTime = BigDecimal.ZERO; + + @Column(name = "P95_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0") + @Comment("95 백분위 응답시간(ms)") + private BigDecimal p95RespTime = BigDecimal.ZERO; +} diff --git a/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java new file mode 100644 index 0000000..1d0a266 --- /dev/null +++ b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java @@ -0,0 +1,26 @@ +package com.eactive.eai.data.entity.custom.statistics; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * API 통계 분단위 복합키 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiStatsMinuteId implements Serializable { + private static final long serialVersionUID = 1L; + + private LocalDateTime statTime; + private String apiName; + private String gwInstanceId; + private String bizDivCode; + private String clientId; + private String inboundAdapter; + private String outboundAdapter; +}