Merge branch 'jenkins_with_weblogic' into devs/sso

# Conflicts:
#	readme.md
#	src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java
This commit is contained in:
Rinjae
2026-01-08 17:47:26 +09:00
302 changed files with 85257 additions and 897 deletions
@@ -0,0 +1,83 @@
# API Gateway 통계 테이블 설계
## 요구사항 정리
### 수집 차원
- **시간 단위**: 분 단위
- **분류 차원**: API명, API Gateway Instance, ClientId, Scope, 업무구분코드 (값이 없을 경우 'NONE' 으로 대체)
### 메트릭
- **처리 건수**:
- 총 건수
- 성공 건수
- 오류 건수 (Timeout / System Error / Business Error)
- **응답시간 통계**:
- 평균 응답시간
- 최소 응답시간
- 최대 응답시간
- **추가 검토사항**:
- P50, P95 백분위 응답시간
### 집계 방식
- **집계 주기**: 매분 배치 집계
- **백분위 계산**: HdrHistogram 라이브러리 사용 (근사값, 메모리 효율적)
---
## 테이블 DDL
```sql
-- API Gateway 분단위 통계 테이블
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, -- 업무구분코드
-- 건수 메트릭
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,
-- 응답시간 메트릭 (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,
-- 관리 정보
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
)
);
-- 코멘트
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.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.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)';
COMMENT ON COLUMN API_STATS_MINUTE.P50_RESP_TIME IS '50 백분위 응답시간(ms)';
COMMENT ON COLUMN API_STATS_MINUTE.P95_RESP_TIME IS '95 백분위 응답시간(ms)';
```
---
@@ -19,8 +19,46 @@ public class KjbPropertyInjector {
this.groupId = groupId;
}
public KjbProperty inject() {
return inject(new KjbProperty());
/**
* KjbProperty의 URL 값들이 설정되어 있는지 확인
* @param property 검사할 KjbProperty 객체
* @return URL 값들이 모두 설정되어 있으면 true
*/
public boolean isUrlConfigured(KjbProperty property) {
if (property == null) return false;
// 주요 URL 값들 중 하나라도 설정되어 있으면 true
return StringUtils.isNotEmpty(property.getEaiBatchUrl())
|| StringUtils.isNotEmpty(property.getUmsHostUrl())
|| StringUtils.isNotEmpty(property.getObpUrl());
}
/**
* URL 값이 비어있을 경우 DB에서 재로딩 시도
* @param property 기존 KjbProperty 객체
* @return 재로딩된 KjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
*/
public KjbProperty reloadIfUrlEmpty(KjbProperty property) {
if (property == null) {
return inject(new KjbProperty());
}
if (!isUrlConfigured(property)) {
log.info("KjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
try {
KjbProperty reloaded = inject(new KjbProperty());
if (isUrlConfigured(reloaded)) {
log.info("KjbProperty 재로딩 성공");
return reloaded;
} else {
log.warn("KjbProperty 재로딩 후에도 URL 값이 비어있음");
}
} catch (Exception e) {
log.error("KjbProperty 재로딩 실패: {}", e.getMessage(), e);
}
}
return property;
}
public <T> T inject(T property) {
@@ -0,0 +1,17 @@
package com.eactive.ext.kjb.web;
import java.util.List;
import lombok.Data;
@Data
public class ApiLogUI {
private String apiServiceNumber;
private String txDate;
private List<ApiMessageLogUI> logs;
private Long totalProcessingTime;
}
@@ -0,0 +1,35 @@
package com.eactive.ext.kjb.web;
import java.util.Map;
import lombok.Data;
@Data
public class ApiMessageLogUI {
private String apiServiceNumber;
private int processNumber;
private String txDate;
private String url;
private String queryString;
private String clientId;
private String method;
private Integer HttpStatus;
private Map<String, String> header;
private Object body;
private String exdata;
private Long processingTime;
private String guid;
}
@@ -0,0 +1,142 @@
package com.eactive.ext.kjb.web;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.data.entity.onl.logger.ApiLogDTO;
import com.eactive.eai.rms.data.entity.onl.logger.ApiMessageLogDTO;
import com.eactive.eai.rms.data.entity.onl.logger.EAILogService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiTransactionLogController implements InterceptorSkipController {
private final EAILogService eaiLogService;
private final ObjectMapper objectMapper = new ObjectMapper();
@GetMapping("/api/transaction/log.do")
@PostMapping("/api/transaction/log.do")
public ResponseEntity<ApiLogUI> getApiLog(String txDate, String apiServiceNumber) {
DataSourceContextHolder.setDataSourceType(DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
// txDate가 없으면 오늘 날짜 설정
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate searchDate;
if (StringUtils.hasText(txDate)) {
searchDate = LocalDate.parse(txDate, formatter);
} else {
searchDate = LocalDate.now();
}
// 최대 10일 전까지 조회 시도
ApiLogDTO dto = null;
for (int i = 0; i < 10; i++) {
String searchDateStr = searchDate.format(formatter);
dto = eaiLogService.getApiLog(searchDateStr, apiServiceNumber);
// 결과가 있으면 종료
if (dto != null && dto.getLogs() != null && !dto.getLogs().isEmpty()) {
break;
}
// 하루 전으로 이동
searchDate = searchDate.minusDays(1);
}
return ResponseEntity.ok(convertToUI(dto));
}
private ApiLogUI convertToUI(ApiLogDTO dto) {
ApiLogUI ui = new ApiLogUI();
ui.setApiServiceNumber(dto.getApiServiceNumber());
ui.setTxDate(dto.getTxDate());
ui.setTotalProcessingTime(dto.getTotalProcessingTime());
if (dto.getLogs() != null) {
List<ApiMessageLogUI> uiLogs = dto.getLogs().stream().map(this::convertMessageToUI)
.collect(Collectors.toList());
ui.setLogs(uiLogs);
}
return ui;
}
private ApiMessageLogUI convertMessageToUI(ApiMessageLogDTO dto) {
ApiMessageLogUI ui = new ApiMessageLogUI();
ui.setApiServiceNumber(dto.getApiServiceNumber());
ui.setProcessNumber(dto.getProcessNumber());
ui.setTxDate(dto.getTxDate());
ui.setUrl(dto.getUrl());
ui.setQueryString(dto.getQueryString());
ui.setClientId(dto.getClientId());
ui.setMethod(dto.getMethod());
ui.setHttpStatus(dto.getHttpStatus());
ui.setExdata(dto.getExdata());
ui.setProcessingTime(dto.getProcessingTime());
ui.setGuid(dto.getKeymgtmsgctnt());
// header: JSON 문자열 → Map<String, String>
if (dto.getHeader() != null) {
try {
List<Map<String, String>> headerList = objectMapper.readValue(dto.getHeader(),
new TypeReference<List<Map<String, String>>>() {
});
Map<String, String> headerMap = new HashMap<>();
for (Map<String, String> headerItem : headerList) {
String name = headerItem.get("name");
String value = headerItem.get("value");
if (name != null) {
headerMap.put(name, value);
}
}
// header에서 client*id 패턴 추출 (정규식, ignoreCase)
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
if (entry.getKey() != null && entry.getKey().matches("(?i)client.*id")) {
ui.setClientId(entry.getValue());
break;
}
}
ui.setHeader(headerMap);
} catch (Exception e) {
log.error("Failed to parse header JSON: {}", dto.getHeader(), e);
}
}
// body: JSON 문자열 → Object
if (dto.getBody() != null) {
try {
Object bodyObject = objectMapper.readValue(dto.getBody(), Object.class);
ui.setBody(bodyObject);
} catch (Exception e) {
// JSON 파싱 실패 시 원본 문자열 사용
ui.setBody(dto.getBody());
}
}
return ui;
}
}
@@ -0,0 +1 @@
package com.eactive.ext.kjb.web;