시간 통계 추가

This commit is contained in:
pksup
2026-01-13 09:15:09 +09:00
parent 9440e3cd9f
commit b7cfd63272
11 changed files with 1131 additions and 116 deletions
@@ -9,27 +9,40 @@ 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.HourStatsKey;
import com.eactive.eai.custom.statistics.dto.MinuteStatsKey;
import com.eactive.eai.custom.statistics.dto.TransactionEvent;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsHour;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 수집기
* - 거래 이벤트를 분단위로 메모리에 집계
* - 거래 이벤트를 분/시간 단위로 메모리에 집계
* - Thread-safe한 ConcurrentHashMap 사용
*/
@Slf4j
@Service
public class ApiStatsCollector {
/** 최대 Aggregator 수 제한 (메모리 보호) */
private static final int MAX_MINUTE_AGGREGATORS = 10000;
private static final int MAX_HOUR_AGGREGATORS = 5000;
/**
* 분단위 집계 맵
* Key: MinuteStatsKey (시간 + 차원들)
* Value: MinuteStatsAggregator (집계 데이터)
*/
private final ConcurrentHashMap<MinuteStatsKey, MinuteStatsAggregator> aggregators = new ConcurrentHashMap<>();
private final ConcurrentHashMap<MinuteStatsKey, MinuteStatsAggregator> minuteAggregators = new ConcurrentHashMap<>();
/**
* 시간별 집계 맵
* Key: HourStatsKey (시간 + 차원들)
* Value: HourStatsAggregator (집계 데이터)
*/
private final ConcurrentHashMap<HourStatsKey, HourStatsAggregator> hourAggregators = new ConcurrentHashMap<>();
/**
* 거래 이벤트 기록
@@ -47,30 +60,70 @@ public class ApiStatsCollector {
* @param eventTime 이벤트 발생 시간
*/
public void recordEvent(TransactionEvent event, LocalDateTime eventTime) {
// 통계 수집 비활성화 체크
if (!ApiStatsConfig.isEnabled()) {
// 통계 수집 비활성화 체크 (분단위 또는 시간별 중 하나라도 활성화되어야 함)
boolean minuteEnabled = ApiStatsConfig.isMinuteEnabled();
boolean hourEnabled = ApiStatsConfig.isHourEnabled();
if (!minuteEnabled && !hourEnabled) {
if (log.isDebugEnabled()) {
log.debug("API stats collection is disabled");
log.debug("API stats collection is disabled (both minute and hour)");
}
return;
}
try {
// 집계 키 생성 (분 단위로 절사)
MinuteStatsKey key = MinuteStatsKey.from(event, eventTime);
MinuteStatsKey minuteKey = null;
MinuteStatsAggregator minuteAggregator = null;
HourStatsKey hourKey = null;
HourStatsAggregator hourAggregator = null;
// 집계기 가져오기 또는 생성
MinuteStatsAggregator aggregator = aggregators.computeIfAbsent(
key,
MinuteStatsAggregator::new
);
// 분단위 집계 (활성화된 경우만)
if (minuteEnabled) {
// Aggregator 개수 제한 체크 (메모리 보호)
if (minuteAggregators.size() >= MAX_MINUTE_AGGREGATORS) {
log.warn("Minute aggregator limit reached ({}), forcing flush of completed minutes", MAX_MINUTE_AGGREGATORS);
flushCompletedMinutes();
// 이벤트 기록
aggregator.recordEvent(event);
// flush 후에도 여전히 제한 초과 시 경고
if (minuteAggregators.size() >= MAX_MINUTE_AGGREGATORS) {
log.error("Minute aggregator count still exceeds limit after flush: {}", minuteAggregators.size());
}
}
minuteKey = MinuteStatsKey.from(event, eventTime);
minuteAggregator = minuteAggregators.computeIfAbsent(
minuteKey,
MinuteStatsAggregator::new
);
minuteAggregator.recordEvent(event);
}
// 시간별 집계 (활성화된 경우만)
if (hourEnabled) {
// Aggregator 개수 제한 체크 (메모리 보호)
if (hourAggregators.size() >= MAX_HOUR_AGGREGATORS) {
log.warn("Hour aggregator limit reached ({}), forcing flush of completed hours", MAX_HOUR_AGGREGATORS);
flushCompletedHours();
// flush 후에도 여전히 제한 초과 시 경고
if (hourAggregators.size() >= MAX_HOUR_AGGREGATORS) {
log.error("Hour aggregator count still exceeds limit after flush: {}", hourAggregators.size());
}
}
hourKey = HourStatsKey.from(event, eventTime);
hourAggregator = hourAggregators.computeIfAbsent(
hourKey,
HourStatsAggregator::new
);
hourAggregator.recordEvent(event);
}
if (log.isDebugEnabled()) {
log.debug("Recorded event: {}, total count: {}",
key, aggregator.getTotalCount());
log.debug("Recorded event: minute={}, hour={}, minute_total={}, hour_total={}",
minuteKey, hourKey,
minuteAggregator != null ? minuteAggregator.getTotalCount() : 0,
hourAggregator != null ? hourAggregator.getTotalCount() : 0);
}
} catch (Exception e) {
@@ -79,20 +132,20 @@ public class ApiStatsCollector {
}
/**
* 지정된 시간 이전의 집계 데이터를 flush하여 반환
* 지정된 시간 이전의 분단위 집계 데이터를 flush하여 반환
* - 반환된 데이터는 메모리에서 제거됨
*
* @param beforeTime 이 시간 이전의 데이터를 flush (exclusive)
* @return 집계된 통계 리스트
*/
public List<ApiStatsMinute> flushBefore(LocalDateTime beforeTime) {
public List<ApiStatsMinute> flushMinuteBefore(LocalDateTime beforeTime) {
LocalDateTime cutoffTime = beforeTime.truncatedTo(ChronoUnit.MINUTES);
List<ApiStatsMinute> result = new ArrayList<>();
// 대상 키 수집 및 스냅샷 생성
List<MinuteStatsKey> keysToRemove = new ArrayList<>();
aggregators.forEach((key, aggregator) -> {
minuteAggregators.forEach((key, aggregator) -> {
if (key.getStatTime().isBefore(cutoffTime)) {
// 스냅샷 생성 (건수가 0보다 큰 경우만)
if (aggregator.getTotalCount() > 0) {
@@ -104,16 +157,50 @@ public class ApiStatsCollector {
});
// 메모리에서 제거
keysToRemove.forEach(aggregators::remove);
keysToRemove.forEach(minuteAggregators::remove);
log.info("Flushed {} stats records before {}, remaining: {}",
result.size(), cutoffTime, aggregators.size());
log.info("Flushed {} minute stats records before {}, remaining: {}",
result.size(), cutoffTime, minuteAggregators.size());
return result;
}
/**
* 현재 분의 집계 데이터를 flush
* 지정된 시간 이전의 시간별 집계 데이터를 flush하여 반환
* - 반환된 데이터는 메모리에서 제거됨
*
* @param beforeTime 이 시간 이전의 데이터를 flush (exclusive)
* @return 집계된 통계 리스트
*/
public List<ApiStatsHour> flushHourBefore(LocalDateTime beforeTime) {
LocalDateTime cutoffTime = beforeTime.truncatedTo(ChronoUnit.HOURS);
List<ApiStatsHour> result = new ArrayList<>();
// 대상 키 수집 및 스냅샷 생성
List<HourStatsKey> keysToRemove = new ArrayList<>();
hourAggregators.forEach((key, aggregator) -> {
if (key.getStatTime().isBefore(cutoffTime)) {
// 스냅샷 생성 (건수가 0보다 큰 경우만)
if (aggregator.getTotalCount() > 0) {
ApiStatsHour snapshot = aggregator.snapshot();
result.add(snapshot);
}
keysToRemove.add(key);
}
});
// 메모리에서 제거
keysToRemove.forEach(hourAggregators::remove);
log.info("Flushed {} hour stats records before {}, remaining: {}",
result.size(), cutoffTime, hourAggregators.size());
return result;
}
/**
* 완료된 분단위 집계 데이터를 flush
* - 현재 분 이전의 모든 데이터를 반환하고 제거
*/
public List<ApiStatsMinute> flushCompletedMinutes() {
@@ -121,32 +208,70 @@ public class ApiStatsCollector {
LocalDateTime now = LocalDateTime.now();
LocalDateTime currentMinute = now.truncatedTo(ChronoUnit.MINUTES);
return flushBefore(currentMinute);
return flushMinuteBefore(currentMinute);
}
/**
* 모든 집계 데이터를 강제로 flush (종료 시 사용)
* 완료된 시간별 집계 데이터를 flush
* - 현재 시간 이전의 모든 데이터를 반환하고 제거
*/
public List<ApiStatsMinute> flushAll() {
public List<ApiStatsHour> flushCompletedHours() {
// 현재 시간은 아직 진행 중이므로 제외
LocalDateTime now = LocalDateTime.now();
LocalDateTime currentHour = now.truncatedTo(ChronoUnit.HOURS);
return flushHourBefore(currentHour);
}
/**
* 모든 분단위 집계 데이터를 강제로 flush (종료 시 사용)
*/
public List<ApiStatsMinute> flushAllMinutes() {
List<ApiStatsMinute> result = new ArrayList<>();
aggregators.forEach((key, aggregator) -> {
minuteAggregators.forEach((key, aggregator) -> {
if (aggregator.getTotalCount() > 0) {
result.add(aggregator.snapshot());
}
});
aggregators.clear();
minuteAggregators.clear();
log.info("Flushed all {} stats records", result.size());
log.info("Flushed all {} minute stats records", result.size());
return result;
}
/**
* 현재 집계 중인 키 개수 반환 (모니터링용)
* 모든 시간별 집계 데이터를 강제로 flush (종료 시 사용)
*/
public int getActiveAggregatorCount() {
return aggregators.size();
public List<ApiStatsHour> flushAllHours() {
List<ApiStatsHour> result = new ArrayList<>();
hourAggregators.forEach((key, aggregator) -> {
if (aggregator.getTotalCount() > 0) {
result.add(aggregator.snapshot());
}
});
hourAggregators.clear();
log.info("Flushed all {} hour stats records", result.size());
return result;
}
/**
* 현재 집계 중인 분단위 키 개수 반환 (모니터링용)
*/
public int getActiveMinuteAggregatorCount() {
return minuteAggregators.size();
}
/**
* 현재 집계 중인 시간별 키 개수 반환 (모니터링용)
*/
public int getActiveHourAggregatorCount() {
return hourAggregators.size();
}
}
@@ -0,0 +1,232 @@
package com.eactive.eai.custom.statistics.collector;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.concurrent.atomic.AtomicLong;
import com.eactive.eai.custom.statistics.dto.HourStatsKey;
import com.eactive.eai.custom.statistics.dto.TransactionEvent;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsHour;
import lombok.Getter;
/**
* 시간별 통계 집계기
* - Bucket 방식으로 응답시간 백분위 계산 (메모리 최적화)
* - Thread-safe한 집계 처리
*/
public class HourStatsAggregator {
@Getter
private final HourStatsKey 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);
// 응답시간 Bucket (10개 구간으로 분류)
private final AtomicLong[] respTimeBuckets = new AtomicLong[10];
// Bucket 경계값 (ms)
private static final long[] BUCKET_BOUNDARIES = {
20, 50, 70, 100, // 빠른 구간 (0-100ms)
200, 300, 500, // 일반 구간 (100-500ms) - P95 주로 여기
1000, 5000, 60000 // 느린 구간 (500ms-60s)
};
// 응답시간 통계용
private final AtomicLong totalRespTime = new AtomicLong(0); // 평균 계산용
private final AtomicLong minRespTime = new AtomicLong(Long.MAX_VALUE);
private final AtomicLong maxRespTime = new AtomicLong(Long.MIN_VALUE);
public HourStatsAggregator(HourStatsKey key) {
this.key = key;
// Bucket 초기화
for (int i = 0; i < respTimeBuckets.length; i++) {
respTimeBuckets[i] = new AtomicLong(0);
}
}
/**
* 거래 이벤트 기록
* - AtomicLong 사용으로 synchronized 불필요 (성능 최적화)
*/
public 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);
// Bucket에 기록
int bucketIndex = findBucketIndex(respTime);
respTimeBuckets[bucketIndex].incrementAndGet();
// 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, lock-free)
*/
private void updateMinMax(long respTime) {
// Min 업데이트 (compare-and-swap)
long currentMin;
while ((currentMin = minRespTime.get()) > respTime) {
if (minRespTime.compareAndSet(currentMin, respTime)) {
break;
}
}
// Max 업데이트 (compare-and-swap)
long currentMax;
while ((currentMax = maxRespTime.get()) < respTime) {
if (maxRespTime.compareAndSet(currentMax, respTime)) {
break;
}
}
}
/**
* 응답시간에 해당하는 Bucket 인덱스 찾기
*/
private int findBucketIndex(long respTime) {
for (int i = 0; i < BUCKET_BOUNDARIES.length; i++) {
if (respTime <= BUCKET_BOUNDARIES[i]) {
return i;
}
}
return BUCKET_BOUNDARIES.length - 1; // 마지막 경계값 초과 시 마지막 bucket
}
/**
* Bucket 기반 백분위 계산
* @param percentile 백분위 (50.0 for P50, 95.0 for P95)
* @return 백분위 응답시간 (ms)
*/
private long calculatePercentile(double percentile) {
long total = totalCnt.get();
if (total == 0) {
return 0;
}
long targetCount = (long) Math.ceil(total * percentile / 100.0);
long accumulated = 0;
for (int i = 0; i < respTimeBuckets.length; i++) {
accumulated += respTimeBuckets[i].get();
if (accumulated >= targetCount) {
// 해당 bucket의 상한값 반환
return BUCKET_BOUNDARIES[i];
}
}
// 모든 bucket을 넘어간 경우
return BUCKET_BOUNDARIES[BUCKET_BOUNDARIES.length - 1];
}
/**
* 현재 집계 결과를 Entity로 변환
*/
public ApiStatsHour snapshot() {
ApiStatsHour stats = new ApiStatsHour();
// 키 정보
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.get()).setScale(3, RoundingMode.HALF_UP));
stats.setMaxRespTime(BigDecimal.valueOf(maxRespTime.get()).setScale(3, RoundingMode.HALF_UP));
// 백분위 (Bucket 기반 계산)
long p50 = calculatePercentile(50.0);
long p95 = calculatePercentile(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();
}
}
@@ -4,9 +4,6 @@ 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;
@@ -15,7 +12,7 @@ import lombok.Getter;
/**
* 분단위 통계 집계기
* - HdrHistogram을 사용하여 응답시간 백분위 계산
* - Bucket 방식으로 응답시간 백분위 계산 (메모리 최적화)
* - Thread-safe한 집계 처리
*/
public class MinuteStatsAggregator {
@@ -35,27 +32,34 @@ public class MinuteStatsAggregator {
private final AtomicLong seq900SystemErrCnt = new AtomicLong(0);
private final AtomicLong seq900BizErrCnt = new AtomicLong(0);
// 응답시간 히스토그램 (1ms ~ 60초, 3자리 정밀도)
private final Histogram histogram;
// 응답시간 Bucket (10개 구간으로 분류)
private final AtomicLong[] respTimeBuckets = new AtomicLong[10];
// Bucket 경계값 (ms)
private static final long[] BUCKET_BOUNDARIES = {
20, 50, 70, 100, // 빠른 구간 (0-100ms)
200, 300, 500, // 일반 구간 (100-500ms) - P95 주로 여기
1000, 5000, 60000 // 느린 구간 (500ms-60s)
};
// 응답시간 통계용
private final AtomicLong totalRespTime = new AtomicLong(0); // 평균 계산용
private volatile long minRespTime = Long.MAX_VALUE;
private volatile long maxRespTime = Long.MIN_VALUE;
private final AtomicLong minRespTime = new AtomicLong(Long.MAX_VALUE);
private final AtomicLong maxRespTime = new AtomicLong(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);
// Bucket 초기화
for (int i = 0; i < respTimeBuckets.length; i++) {
respTimeBuckets[i] = new AtomicLong(0);
}
}
/**
* 거래 이벤트 기록
* - AtomicLong 사용으로 synchronized 불필요 (성능 최적화)
*/
public synchronized void recordEvent(TransactionEvent event) {
public void recordEvent(TransactionEvent event) {
int logSeq = event.getLogSequence();
// 로그 시퀀스 400: 일반 통계 집계
@@ -82,14 +86,9 @@ public class MinuteStatsAggregator {
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);
}
// Bucket에 기록
int bucketIndex = findBucketIndex(respTime);
respTimeBuckets[bucketIndex].incrementAndGet();
// Min/Max 업데이트
updateMinMax(respTime);
@@ -112,17 +111,64 @@ public class MinuteStatsAggregator {
}
/**
* Min/Max 응답시간 업데이트 (thread-safe)
* Min/Max 응답시간 업데이트 (thread-safe, lock-free)
*/
private synchronized void updateMinMax(long respTime) {
if (respTime < minRespTime) {
minRespTime = respTime;
private void updateMinMax(long respTime) {
// Min 업데이트 (compare-and-swap)
long currentMin;
while ((currentMin = minRespTime.get()) > respTime) {
if (minRespTime.compareAndSet(currentMin, respTime)) {
break;
}
}
if (respTime > maxRespTime) {
maxRespTime = respTime;
// Max 업데이트 (compare-and-swap)
long currentMax;
while ((currentMax = maxRespTime.get()) < respTime) {
if (maxRespTime.compareAndSet(currentMax, respTime)) {
break;
}
}
}
/**
* 응답시간에 해당하는 Bucket 인덱스 찾기
*/
private int findBucketIndex(long respTime) {
for (int i = 0; i < BUCKET_BOUNDARIES.length; i++) {
if (respTime <= BUCKET_BOUNDARIES[i]) {
return i;
}
}
return BUCKET_BOUNDARIES.length - 1; // 마지막 경계값 초과 시 마지막 bucket
}
/**
* Bucket 기반 백분위 계산
* @param percentile 백분위 (50.0 for P50, 95.0 for P95)
* @return 백분위 응답시간 (ms)
*/
private long calculatePercentile(double percentile) {
long total = totalCnt.get();
if (total == 0) {
return 0;
}
long targetCount = (long) Math.ceil(total * percentile / 100.0);
long accumulated = 0;
for (int i = 0; i < respTimeBuckets.length; i++) {
accumulated += respTimeBuckets[i].get();
if (accumulated >= targetCount) {
// 해당 bucket의 상한값 반환
return BUCKET_BOUNDARIES[i];
}
}
// 모든 bucket을 넘어간 경우
return BUCKET_BOUNDARIES[BUCKET_BOUNDARIES.length - 1];
}
/**
* 현재 집계 결과를 Entity로 변환
*/
@@ -158,12 +204,12 @@ public class MinuteStatsAggregator {
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));
stats.setMinRespTime(BigDecimal.valueOf(minRespTime.get()).setScale(3, RoundingMode.HALF_UP));
stats.setMaxRespTime(BigDecimal.valueOf(maxRespTime.get()).setScale(3, RoundingMode.HALF_UP));
// 백분위 (HdrHistogram 사용)
long p50 = histogram.getValueAtPercentile(50.0);
long p95 = histogram.getValueAtPercentile(95.0);
// 백분위 (Bucket 기반 계산)
long p50 = calculatePercentile(50.0);
long p95 = calculatePercentile(95.0);
stats.setP50RespTime(BigDecimal.valueOf(p50).setScale(3, RoundingMode.HALF_UP));
stats.setP95RespTime(BigDecimal.valueOf(p95).setScale(3, RoundingMode.HALF_UP));
} else {
@@ -21,12 +21,11 @@ public class ApiStatsConfig {
/** 기본값 상수 */
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;
private static final boolean DEFAULT_MINUTE_ENABLED = true;
private static final boolean DEFAULT_HOUR_ENABLED = true;
/**
* 통계 수집 활성화 여부
* 통계 수집 활성화 여부 (전체 ON/OFF)
* - true: 통계 수집 활성화 (기본값)
* - false: 통계 수집 비활성화
*/
@@ -42,44 +41,42 @@ public class ApiStatsConfig {
}
/**
* 히스토그램 최소값 (ms)
* 분단위 통계 수집 활성화 여부
* - true: 분단위 통계 수집 활성화 (기본값)
* - false: 분단위 통계 수집 비활성화
* - 전체 enabled가 false이면 이 설정과 무관하게 비활성화됨
*/
public static long getHistogramMinMs() {
public static boolean isMinuteEnabled() {
if (!isEnabled()) {
return false;
}
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);
String value = prop.getProperty("minute.enabled", String.valueOf(DEFAULT_MINUTE_ENABLED));
return Boolean.parseBoolean(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;
logger.debug("Failed to get minute.enabled from PropManager, using default: " + DEFAULT_MINUTE_ENABLED);
return DEFAULT_MINUTE_ENABLED;
}
}
/**
* 히스토그램 최대값 (ms)
* 시간별 통계 수집 활성화 여부
* - true: 시간별 통계 수집 활성화 (기본값)
* - false: 시간별 통계 수집 비활성화
* - 전체 enabled가 false이면 이 설정과 무관하게 비활성화됨
*/
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 boolean isHourEnabled() {
if (!isEnabled()) {
return false;
}
}
/**
* 히스토그램 정밀도 (유효숫자)
*/
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);
String value = prop.getProperty("hour.enabled", String.valueOf(DEFAULT_HOUR_ENABLED));
return Boolean.parseBoolean(value);
} catch (Exception e) {
logger.debug("Failed to get histogram.precision from PropManager, using default: " + DEFAULT_HISTOGRAM_PRECISION);
return DEFAULT_HISTOGRAM_PRECISION;
logger.debug("Failed to get hour.enabled from PropManager, using default: " + DEFAULT_HOUR_ENABLED);
return DEFAULT_HOUR_ENABLED;
}
}
@@ -88,8 +85,7 @@ public class ApiStatsConfig {
*/
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());
logger.info("ApiStatsConfig] minute.enabled = " + isMinuteEnabled());
logger.info("ApiStatsConfig] hour.enabled = " + isHourEnabled());
}
}
@@ -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 HourStatsKey {
/** 통계 시간 (시간 단위, 분/초는 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 HourStatsKey(LocalDateTime statTime, String apiName, String gwInstanceId,
String bizDivCode, String clientId, String inboundAdapter, String outboundAdapter) {
// 분, 초, 나노초를 0으로 절사하여 시간 단위로 정규화
this.statTime = statTime.truncatedTo(ChronoUnit.HOURS);
this.apiName = apiName;
this.gwInstanceId = gwInstanceId;
this.bizDivCode = bizDivCode;
this.clientId = clientId;
this.inboundAdapter = inboundAdapter;
this.outboundAdapter = outboundAdapter;
}
/**
* TransactionEvent로부터 키 생성
*/
public static HourStatsKey from(TransactionEvent event, LocalDateTime eventTime) {
return new HourStatsKey(
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;
HourStatsKey that = (HourStatsKey) 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);
}
}
@@ -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.ApiStatsHour;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsHourId;
import com.eactive.eai.data.jpa.AbstractDataLogger;
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class ApiStatsHourLogger
extends AbstractDataLogger<ApiStatsHour, ApiStatsHourId, ApiStatsHourRepository> {
}
@@ -0,0 +1,9 @@
package com.eactive.eai.custom.statistics.persistence;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsHour;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsHourId;
import com.eactive.eai.data.jpa.BaseRepository;
public interface ApiStatsHourRepository extends BaseRepository<ApiStatsHour, ApiStatsHourId> {
}
@@ -10,7 +10,9 @@ 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.ApiStatsHourLogger;
import com.eactive.eai.custom.statistics.persistence.ApiStatsMinuteLogger;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsHour;
import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute;
import lombok.RequiredArgsConstructor;
@@ -18,7 +20,8 @@ import lombok.extern.slf4j.Slf4j;
/**
* API 통계 스케줄러
* - 매 분마다 실행되어 집계된 통계를 DB에 저장
* - 분단위: 매 분 5초에 실행하여 집계된 통계를 DB에 저장
* - 시간별: 매 시간 5분에 실행하여 집계된 통계를 DB에 저장
*/
@Slf4j
@Component
@@ -26,7 +29,12 @@ import lombok.extern.slf4j.Slf4j;
public class ApiStatsScheduler {
private final ApiStatsCollector collector;
private final ApiStatsMinuteLogger logger;
private final ApiStatsMinuteLogger minuteLogger;
private final ApiStatsHourLogger hourLogger;
// 이전 설정값 추적 (설정 변경 시 flush용)
private volatile boolean previousMinuteEnabled = true;
private volatile boolean previousHourEnabled = true;
/**
* 초기화 시 설정값 로깅
@@ -38,14 +46,28 @@ public class ApiStatsScheduler {
}
/**
* 매 분마다 실행 (매 분 5초에 실행)
* 매 분마다 실행 (매 분 10초에 실행)
* - 이전 분의 완료된 통계를 DB에 저장
* - 10초 여유를 두어 이전 분 데이터가 완전히 수집되도록 함
*/
@Scheduled(cron = "5 * * * * ?")
@Scheduled(cron = "10 * * * * ?")
public void saveMinuteStats() {
// 통계 수집 비활성화 체크
if (!ApiStatsConfig.isEnabled()) {
log.debug("API stats collection is disabled, skipping save");
boolean currentEnabled = ApiStatsConfig.isMinuteEnabled();
// 설정이 enabled → disabled로 변경된 경우: 남은 데이터 모두 flush
if (previousMinuteEnabled && !currentEnabled) {
log.info("Minute stats disabled, flushing all remaining data...");
flushAllMinuteStats();
previousMinuteEnabled = currentEnabled;
return;
}
// 현재 상태 업데이트
previousMinuteEnabled = currentEnabled;
// 분단위 통계 수집 비활성화 체크
if (!currentEnabled) {
log.debug("API minute stats collection is disabled, skipping save");
return;
}
@@ -66,11 +88,11 @@ public class ApiStatsScheduler {
for (ApiStatsMinute stats : statsList) {
try {
logger.log(stats);
minuteLogger.log(stats);
savedCount++;
if (log.isDebugEnabled()) {
log.debug("Saved stats: {} - total={}, success={}, p95={}ms",
log.debug("Saved minute stats: {} - total={}, success={}, p95={}ms",
stats.getApiName(),
stats.getTotalCnt(),
stats.getSuccessCnt(),
@@ -79,18 +101,150 @@ public class ApiStatsScheduler {
} catch (Exception e) {
errorCount++;
log.error("Failed to save stats: {}", stats, e);
log.error("Failed to save minute stats: {}", stats, e);
}
}
log.info("Minute stats saved: success={}, error={}, active_aggregators={}",
savedCount, errorCount, collector.getActiveAggregatorCount());
log.info("Minute stats saved: success={}, error={}, active_minute_aggregators={}",
savedCount, errorCount, collector.getActiveMinuteAggregatorCount());
} catch (Exception e) {
log.error("Failed to flush minute stats", e);
}
}
/**
* 매 시간마다 실행 (매 시간 10분에 실행)
* - 이전 시간의 완료된 통계를 DB에 저장
* - 10분 여유를 두어 이전 시간 데이터가 완전히 수집되도록 함
*/
@Scheduled(cron = "0 10 * * * ?")
public void saveHourStats() {
boolean currentEnabled = ApiStatsConfig.isHourEnabled();
// 설정이 enabled → disabled로 변경된 경우: 남은 데이터 모두 flush
if (previousHourEnabled && !currentEnabled) {
log.info("Hour stats disabled, flushing all remaining data...");
flushAllHourStats();
previousHourEnabled = currentEnabled;
return;
}
// 현재 상태 업데이트
previousHourEnabled = currentEnabled;
// 시간별 통계 수집 비활성화 체크
if (!currentEnabled) {
log.debug("API hour stats collection is disabled, skipping save");
return;
}
try {
log.debug("Starting hour stats flush...");
// 완료된 시간의 통계 데이터 가져오기
List<ApiStatsHour> statsList = collector.flushCompletedHours();
if (statsList.isEmpty()) {
log.debug("No hour stats to save");
return;
}
// DB에 저장
int savedCount = 0;
int errorCount = 0;
for (ApiStatsHour stats : statsList) {
try {
hourLogger.log(stats);
savedCount++;
if (log.isDebugEnabled()) {
log.debug("Saved hour stats: {} - total={}, success={}, p95={}ms",
stats.getApiName(),
stats.getTotalCnt(),
stats.getSuccessCnt(),
stats.getP95RespTime());
}
} catch (Exception e) {
errorCount++;
log.error("Failed to save hour stats: {}", stats, e);
}
}
log.info("Hour stats saved: success={}, error={}, active_hour_aggregators={}",
savedCount, errorCount, collector.getActiveHourAggregatorCount());
} catch (Exception e) {
log.error("Failed to flush hour stats", e);
}
}
/**
* 분단위 통계 모두 flush (설정 변경 시)
*/
private void flushAllMinuteStats() {
try {
List<ApiStatsMinute> statsList = collector.flushAllMinutes();
if (statsList.isEmpty()) {
log.info("No minute stats to flush");
return;
}
int savedCount = 0;
int errorCount = 0;
for (ApiStatsMinute stats : statsList) {
try {
minuteLogger.log(stats);
savedCount++;
} catch (Exception e) {
errorCount++;
log.error("Failed to save minute stats during config change: {}", stats, e);
}
}
log.info("Flushed all minute stats due to config change: success={}, error={}", savedCount, errorCount);
} catch (Exception e) {
log.error("Failed to flush all minute stats", e);
}
}
/**
* 시간별 통계 모두 flush (설정 변경 시)
*/
private void flushAllHourStats() {
try {
List<ApiStatsHour> statsList = collector.flushAllHours();
if (statsList.isEmpty()) {
log.info("No hour stats to flush");
return;
}
int savedCount = 0;
int errorCount = 0;
for (ApiStatsHour stats : statsList) {
try {
hourLogger.log(stats);
savedCount++;
} catch (Exception e) {
errorCount++;
log.error("Failed to save hour stats during config change: {}", stats, e);
}
}
log.info("Flushed all hour stats due to config change: success={}, error={}", savedCount, errorCount);
} catch (Exception e) {
log.error("Failed to flush all hour stats", e);
}
}
/**
* 애플리케이션 종료 시 남은 통계 저장
*/
@@ -99,17 +253,30 @@ public class ApiStatsScheduler {
try {
log.info("Shutting down ApiStatsScheduler, flushing remaining stats...");
List<ApiStatsMinute> statsList = collector.flushAll();
if (!statsList.isEmpty()) {
for (ApiStatsMinute stats : statsList) {
// 분단위 통계 저장
List<ApiStatsMinute> minuteStatsList = collector.flushAllMinutes();
if (!minuteStatsList.isEmpty()) {
for (ApiStatsMinute stats : minuteStatsList) {
try {
logger.log(stats);
minuteLogger.log(stats);
} catch (Exception e) {
log.error("Failed to save stats during shutdown: {}", stats, e);
log.error("Failed to save minute stats during shutdown: {}", stats, e);
}
}
log.info("Saved {} remaining stats during shutdown", statsList.size());
log.info("Saved {} remaining minute stats during shutdown", minuteStatsList.size());
}
// 시간별 통계 저장
List<ApiStatsHour> hourStatsList = collector.flushAllHours();
if (!hourStatsList.isEmpty()) {
for (ApiStatsHour stats : hourStatsList) {
try {
hourLogger.log(stats);
} catch (Exception e) {
log.error("Failed to save hour stats during shutdown: {}", stats, e);
}
}
log.info("Saved {} remaining hour stats during shutdown", hourStatsList.size());
}
} catch (Exception e) {
@@ -0,0 +1,190 @@
# API 통계 수집 메모리 사용량 예측
## 개요
API 통계 수집 시스템은 분단위/시간별로 메모리에서 집계 후 DB에 저장합니다.
이 문서는 메모리 사용량을 예측하고, 안전한 운영을 위한 제한값을 설명합니다.
## Aggregator 메모리 구성
### 1. Key 메모리 (MinuteStatsKey / HourStatsKey)
| 필드 | 타입 | 평균 크기 | 메모리 |
|------|------|----------|--------|
| statTime | LocalDateTime | - | 24 bytes |
| apiName | String | 50자 | ~124 bytes |
| gwInstanceId | String | 20자 | ~64 bytes |
| bizDivCode | String | 10자 | ~44 bytes |
| clientId | String | 20자 | ~64 bytes |
| inboundAdapter | String | 20자 | ~64 bytes |
| outboundAdapter | String | 20자 | ~64 bytes |
| 객체 오버헤드 | - | - | ~16 bytes |
| **합계** | | | **~464 bytes** |
### 2. Aggregator 메모리 (MinuteStatsAggregator / HourStatsAggregator)
| 구성요소 | 개수/크기 | 메모리 |
|---------|----------|--------|
| AtomicLong (건수 메트릭) | 8개 | 64 bytes |
| AtomicLong (900 에러 카운트) | 3개 | 24 bytes |
| AtomicLong[] (Bucket) | 10개 | 80 bytes |
| long (min/max/total) | 3개 | 24 bytes |
| 기타 필드 및 오버헤드 | - | ~592 bytes |
| **합계** | | **~784 bytes** |
### 3. Aggregator 1개당 총 메모리
```
Key + Aggregator = 464 + 784 = 약 1.2KB
```
## 메모리 사용량 시나리오
### 시나리오 1: 일반적인 경우
**가정:**
- API 종류: 100개
- 클라이언트: 50개
- 평균 차원 조합: 200개 (동시에 활성화된 조합)
**메모리 사용량:**
- 분단위: 200개 × 1.2KB = **240KB**
- 시간별: 200개 × 1.2KB = **240KB**
- **합계**: **약 480KB**
### 시나리오 2: 높은 부하
**가정:**
- API 종류: 500개
- 클라이언트: 200개
- 평균 차원 조합: 2,000개
**메모리 사용량:**
- 분단위: 2,000개 × 1.2KB = **2.4MB**
- 시간별: 2,000개 × 1.2KB = **2.4MB**
- **합계**: **약 4.8MB**
### 시나리오 3: 최대 제한 도달 (비정상)
**가정:**
- 분단위 aggregator: 10,000개 (최대 제한)
- 시간별 aggregator: 5,000개 (최대 제한)
**메모리 사용량:**
- 분단위: 10,000개 × 1.2KB = **12MB**
- 시간별: 5,000개 × 1.2KB = **6MB**
- **합계**: **약 18MB**
> ⚠️ **주의**: 이 시나리오는 비정상 상황입니다. 정상적으로는 스케줄러가 주기적으로 flush하여 aggregator 수를 낮게 유지합니다.
## 메모리 보호 메커니즘
### 1. 최대 Aggregator 수 제한
`ApiStatsCollector.java`:
```java
private static final int MAX_MINUTE_AGGREGATORS = 10000;
private static final int MAX_HOUR_AGGREGATORS = 5000;
```
### 2. 자동 Flush
Aggregator 개수가 제한에 도달하면:
1. WARN 로그 출력
2. 완료된 시간의 데이터 자동 flush
3. 메모리 확보
### 3. 스케줄러 주기
- **분단위**: 매 분 5초에 이전 분 데이터 flush
- **시간별**: 매 시간 5분에 이전 시간 데이터 flush
정상 작동 시 aggregator는 주로 현재 시간대 데이터만 유지합니다.
## 차원 조합 수 계산
### 차원 구조
```
차원 조합 = API × GW Instance × 업무구분 × Client × Inbound × Outbound
```
### 예시 계산
**케이스 1: 중소규모**
- API: 100개
- GW Instance: 3개
- 업무구분: 5개
- Client: 10개
- Inbound: 5개
- Outbound: 5개
- **이론적 최대**: 100 × 3 × 5 × 10 × 5 × 5 = 375,000 조합
> 💡 **실제로는 활성화된 조합만 메모리에 존재**하므로 훨씬 적습니다. (일반적으로 수백~수천 개)
**케이스 2: 대규모**
- API: 500개
- 활성 조합 비율: 1% (실제 사용되는 조합)
- **실제 조합**: 약 3,750개
- **분단위 메모리**: 3,750 × 1.2KB = **4.5MB**
- **시간별 메모리**: 3,750 × 1.2KB = **4.5MB**
- **합계**: **약 9MB**
## 모니터링 지표
### 정상 범위
- 분단위 aggregator: **100~1,000개**
- 시간별 aggregator: **100~1,000개**
- 메모리 사용량: **1~5MB**
### 경고 범위
- 분단위 aggregator: **1,000~5,000개**
- 시간별 aggregator: **1,000~3,000개**
- 메모리 사용량: **5~10MB**
### 위험 범위 (조치 필요)
- 분단위 aggregator: **5,000개 이상**
- 시간별 aggregator: **3,000개 이상**
- 메모리 사용량: **10MB 이상**
> ⚠️ 위험 범위 진입 시 차원 수를 줄이거나 제한값 조정을 검토해야 합니다.
## 메모리 최적화 효과
### Bucket 방식 적용 전후 비교 (참고)
**이전 방식의 문제점:**
- Aggregator당 약 2.6MB 사용 (HdrHistogram 포함)
- 1,000개 시: 약 2.6GB
- 10,000개 시: 약 26GB
**현재 방식:**
- Aggregator당 약 1.2KB 사용 (Bucket 배열)
- 1,000개 시: 약 1.2MB
- 10,000개 시: 약 12MB
**메모리 절감**: **99.95% 감소**
## 권장 사항
### 1. 모니터링
- `ApiStatsCollector.getActiveMinuteAggregatorCount()` 주기적 확인
- `ApiStatsCollector.getActiveHourAggregatorCount()` 주기적 확인
- WARN/ERROR 로그 모니터링
### 2. 차원 최적화
- 불필요한 차원 제거 (예: null 값 정규화)
- 고카디널리티 차원 주의 (clientId 등)
### 3. 제한값 조정
운영 환경에 따라 `ApiStatsCollector.java`의 상수 조정:
```java
private static final int MAX_MINUTE_AGGREGATORS = 10000; // 조정 가능
private static final int MAX_HOUR_AGGREGATORS = 5000; // 조정 가능
```
### 4. 설정 관리
PropManager를 통해 동적으로 활성화/비활성화:
- `ApiStats.enabled`: 전체 ON/OFF
- `ApiStats.minute.enabled`: 분단위 ON/OFF
- `ApiStats.hour.enabled`: 시간별 ON/OFF
설정 비활성화 시 메모리의 모든 데이터가 자동으로 flush됩니다.
@@ -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_HOUR")
@org.hibernate.annotations.Table(appliesTo = "API_STATS_HOUR", comment = "API Gateway 시간별 처리 통계")
@IdClass(ApiStatsHourId.class)
public class ApiStatsHour 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;
}
@@ -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 ApiStatsHourId 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;
}