From cfeda1b7af75c6e169b7e4d3017d10973cac18a7 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Fri, 5 Dec 2025 17:29:30 +0900 Subject: [PATCH] gw metric --- .../oracle/sqlMapConfigOfOnl.xml | 3 + .../entity/onl/apim/obp/ObpGwMetricDAO.java | 103 +++++++ .../onl/apim/obp/ObpGwMetricRepository.java | 39 +++ .../onl/apim/obp/ObpGwMetricService.java | 54 ++++ .../entity/onl/apim/obp/ObpGwMetricVO.java | 68 +++++ .../onl/apim/obp/ObpGwMetricController.java | 97 +++++++ .../eai/rms/onl/apim/obp/ObpGwMetricDTO.java | 98 +++++++ .../onl/apim/obp/ObpGwMetricManService.java | 257 ++++++++++++++++++ .../common/service/ObpGwMetricHourJob.java | 56 ++++ .../eai/apim/obp/ObpGwMetric-oracle.xml | 91 +++++++ 10 files changed, 866 insertions(+) create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java create mode 100644 src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml diff --git a/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml b/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml index 8e843a1..0355c0c 100644 --- a/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml +++ b/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml @@ -201,4 +201,7 @@ + + + \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java new file mode 100644 index 0000000..b61fbac --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java @@ -0,0 +1,103 @@ +package com.eactive.eai.rms.data.entity.onl.apim.obp; + +import com.eactive.eai.rms.common.base.SqlMapClientTemplateDao; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * ObpGwMetric DAO - iBATIS를 사용한 로그 테이블 집계 + */ +@Repository +public class ObpGwMetricDAO extends SqlMapClientTemplateDao { + + /** + * EAIBZWKDSTCD 코드 목록 조회 + */ + @SuppressWarnings("unchecked") + public List selectAllBzwkCodes(String schemaId) { + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", schemaId); + return this.template.queryForList("ObpGwMetric.selectAllBzwkCodes", paramMap); + } + + /** + * EAIBZWKDSTCD별 시간대 로그 집계 + */ + @SuppressWarnings("unchecked") + public List selectHourlyMetricsByBzwkCode(HashMap paramMap) { + return this.template.queryForList("ObpGwMetric.selectHourlyMetricsByBzwkCode", paramMap); + } + + /** + * API 이름 조회 (TSEAIHE01) + */ + @SuppressWarnings("unchecked") + public Map selectApiNames(String schemaId, Set apiIds) { + if (apiIds == null || apiIds.isEmpty()) { + return new HashMap<>(); + } + + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", schemaId); + paramMap.put("apiIds", apiIds); + + List> resultList = this.template.queryForList("ObpGwMetric.selectApiNames", paramMap); + + return resultList.stream() + .collect(Collectors.toMap( + m -> m.get("API_ID"), + m -> m.get("API_NAME"), + (v1, v2) -> v1 + )); + } + + /** + * URI 조회 (TSEAIHS04) + */ + @SuppressWarnings("unchecked") + public Map selectUris(String schemaId, Set apiIds) { + if (apiIds == null || apiIds.isEmpty()) { + return new HashMap<>(); + } + + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", schemaId); + paramMap.put("apiIds", apiIds); + + List> resultList = this.template.queryForList("ObpGwMetric.selectUris", paramMap); + + return resultList.stream() + .collect(Collectors.toMap( + m -> m.get("API_ID"), + m -> m.get("URI"), + (v1, v2) -> v1 + )); + } + + /** + * AppId 조회 (ptl_app_request) - org_id로 조회 + */ + @SuppressWarnings("unchecked") + public Map selectAppIdsByOrgIds(Set orgIds) { + if (orgIds == null || orgIds.isEmpty()) { + return new HashMap<>(); + } + + HashMap paramMap = new HashMap<>(); + paramMap.put("orgIds", orgIds); + + List> resultList = this.template.queryForList("ObpGwMetric.selectAppIdsByOrgIds", paramMap); + + return resultList.stream() + .collect(Collectors.toMap( + m -> m.get("ORG_ID"), + m -> m.get("APP_ID"), + (v1, v2) -> v1 + )); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java new file mode 100644 index 0000000..58c5031 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java @@ -0,0 +1,39 @@ +package com.eactive.eai.rms.data.entity.onl.apim.obp; + +import com.eactive.apim.portal.obp.entity.ObpGwMetric; +import com.eactive.eai.data.jpa.BaseRepository; +import com.eactive.eai.rms.data.EMSDataSource; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * ObpGwMetric Repository (eapim-admin 위치) + * API Gateway 시간별 거래량 지표 조회/저장을 위한 Spring Data JPA Repository + */ +@EMSDataSource +public interface ObpGwMetricRepository extends BaseRepository { + + /** + * UPSERT 판단을 위한 기존 데이터 조회 + * 동일한 timeslice + apiId + clientId + hostname + orgId 조합이 존재하는지 확인 + */ + Optional findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + LocalDateTime timeslice, + String apiId, + String clientId, + String hostname, + String orgId + ); + + /** + * 특정 시간대의 모든 GwMetric 데이터 조회 + */ + List findByTimeslice(LocalDateTime timeslice); + + /** + * 특정 시간 범위의 GwMetric 데이터 조회 + */ + List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime); +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java new file mode 100644 index 0000000..79d3022 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java @@ -0,0 +1,54 @@ +package com.eactive.eai.rms.data.entity.onl.apim.obp; + +import com.eactive.apim.portal.obp.entity.ObpGwMetric; +import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * ObpGwMetric Service - 엔티티 CRUD 서비스 + */ +@Service +@Transactional(transactionManager = "transactionManagerForEMS") +public class ObpGwMetricService extends AbstractEMSDataSerivce { + + /** + * UPSERT 판단을 위한 기존 데이터 조회 + */ + public Optional findByUniqueKey(LocalDateTime timeslice, String apiId, String clientId, String hostname, String orgId) { + return getRepository().findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + timeslice, apiId, clientId, hostname, orgId); + } + + /** + * 특정 시간대의 모든 GwMetric 데이터 조회 + */ + public List findByTimeslice(LocalDateTime timeslice) { + return getRepository().findByTimeslice(timeslice); + } + + /** + * 특정 시간 범위의 GwMetric 데이터 조회 + */ + public List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime) { + return getRepository().findByTimesliceBetween(startTime, endTime); + } + + /** + * 엔티티 저장 (INSERT 또는 UPDATE) + */ + public ObpGwMetric save(ObpGwMetric entity) { + return getRepository().save(entity); + } + + /** + * 엔티티 목록 저장 + */ + public List saveAll(List entities) { + return getRepository().saveAll(entities); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java new file mode 100644 index 0000000..20adc9a --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java @@ -0,0 +1,68 @@ +package com.eactive.eai.rms.data.entity.onl.apim.obp; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * ObpGwMetric VO - 로그 집계 결과 매핑용 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ObpGwMetricVO { + + private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000"); + + /** API ID (EAISVCNAME) */ + private String apiId; + + /** 클라이언트 ID (CLIENTID) */ + private String clientId; + + /** 호스트 이름 (EAISEVRINSTNCNAME) */ + private String hostname; + + /** 기관 ID (ORGID) */ + private String orgId; + + /** 타임슬라이스 문자열 (yyyyMMddHH0000000) */ + private String timeslice; + + /** 시도 횟수 (LOGPRCSSSERNO='100' 건수) */ + private Long attemptedCount; + + /** 완료 횟수 (LOGPRCSSSERNO='400' AND 성공 조건 건수) */ + private Long completedCount; + + /** + * timeslice 문자열을 LocalDateTime으로 변환 + */ + public LocalDateTime getTimesliceAsLocalDateTime() { + if (timeslice == null || timeslice.isEmpty()) { + return null; + } + // yyyyMMddHH0000000 형식에서 앞 10자리(yyyyMMddHH)만 사용 + String dateTimeStr = timeslice.substring(0, 10) + "0000"; + return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + } + + /** + * 집계 키 생성 (apiId + clientId + hostname + orgId) + */ + public String getAggregationKey() { + return String.join("|", + nullSafe(apiId), + nullSafe(clientId), + nullSafe(hostname), + nullSafe(orgId) + ); + } + + private String nullSafe(String value) { + return value != null ? value : ""; + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java new file mode 100644 index 0000000..fb3168b --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java @@ -0,0 +1,97 @@ +package com.eactive.eai.rms.onl.apim.obp; + +import com.eactive.eai.rms.common.interceptor.InterceptorSkipController; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * ObpGwMetric REST Controller + * API Gateway 시간별 거래량 지표 조회 API + * + *

세션 체크를 우회하기 위해 InterceptorSkipController 인터페이스 구현

+ */ +@Controller +public class ObpGwMetricController implements InterceptorSkipController { + + private static final Logger log = LoggerFactory.getLogger(ObpGwMetricController.class); + + private static final DateTimeFormatter PATH_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss"); + + private final ObpGwMetricManService obpGwMetricManService; + + public ObpGwMetricController(ObpGwMetricManService obpGwMetricManService) { + this.obpGwMetricManService = obpGwMetricManService; + } + + /** + * 특정 시간대의 GwMetric 데이터 조회 + * + * @param datetime 조회 시간대 (형식: yyyy-MM-dd_HH:mm:ss) + * @return JSON Array + */ + @RequestMapping( + value = "/kjb/gw-metrics/{datetime}", + produces = "application/json;charset=utf-8" + ) + @ResponseBody + public String getGwMetrics(@PathVariable("datetime") String datetime) { + log.debug("GwMetric 조회 요청: datetime={}", datetime); + long startTime = System.currentTimeMillis(); + + try { + // Path 파라미터 파싱 + LocalDateTime targetTime = LocalDateTime.parse(datetime, PATH_FORMATTER); + LocalDateTime timeslice = targetTime.withMinute(0).withSecond(0).withNano(0); + + // 조회 + List metrics = obpGwMetricManService.findByTimeslice(timeslice); + + // JSON 변환 (GSON) + Gson gson = new GsonBuilder() + .serializeNulls() + .create(); + + String json = gson.toJson(metrics); + + // 응답 크기 계산 (UTF-8 기준) + int jsonBytes = json.getBytes(StandardCharsets.UTF_8).length; + String jsonSize = formatByteSize(jsonBytes); + + long elapsed = System.currentTimeMillis() - startTime; + log.info("GwMetric 조회 완료: datetime={}, count={}, size={}, elapsed={}ms", + datetime, metrics.size(), jsonSize, elapsed); + + return json; + + } catch (Exception e) { + log.error("GwMetric 조회 실패: datetime={}", datetime, e); + throw e; + } + } + + /** + * 바이트 크기를 읽기 쉬운 형식으로 변환 + * 예: 1024 -> "1.0 KB", 1048576 -> "1.0 MB" + */ + private String formatByteSize(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return String.format("%.1f KB", bytes / 1024.0); + } else { + return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); + } + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java new file mode 100644 index 0000000..02e8785 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java @@ -0,0 +1,98 @@ +package com.eactive.eai.rms.onl.apim.obp; + +import com.eactive.apim.portal.obp.entity.ObpGwMetric; +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * ObpGwMetric JSON 응답 DTO + * Entity 주석에 명시된 JSON Key 사용 + */ +@Data +public class ObpGwMetricDTO { + + private static final DateTimeFormatter ISO8601_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+0000'"); + + @SerializedName("appKey") + private String clientId; + + @SerializedName("clusterHostname") + private String hostname; + + @SerializedName("timeslice") + private String timeslice; + + @SerializedName("apiName") + private String apiName; + + @SerializedName("apiRoutingUri") + private String uri; + + @SerializedName("attemptedCount") + private Long attemptedCount; + + @SerializedName("completedCount") + private Long completedCount; + + // EAPIM ID + @SerializedName("apiIdByEapim") + private String apiId; + + @SerializedName("appIdByEapim") + private String appId; + + @SerializedName("orgIdByEapim") + private String orgId; + + // AS-IS 호환용 필드 + @SerializedName("appId") + private String appIdByCaPortal; + + @SerializedName("portalOrgId") + private String orgIdByCaPortal; + + @SerializedName("portalApiId") + private String apiIdByCaPortal; + + @SerializedName("partnerCode") + private String partnerCode; + + @SerializedName("apiId") + private String apiIdByCa; + + /** + * Entity → DTO 변환 + */ + public static ObpGwMetricDTO from(ObpGwMetric entity) { + ObpGwMetricDTO dto = new ObpGwMetricDTO(); + dto.setClientId(entity.getClientId()); + dto.setHostname(entity.getHostname()); + dto.setTimeslice(formatTimeslice(entity.getTimeslice())); + dto.setApiName(entity.getApiName()); + dto.setUri(entity.getUri()); + dto.setAttemptedCount(entity.getAttemptedCount()); + dto.setCompletedCount(entity.getCompletedCount()); + dto.setApiId(entity.getApiId()); + dto.setAppId(entity.getAppId()); + dto.setOrgId(entity.getOrgId()); + dto.setAppIdByCaPortal(entity.getAppIdByCaPortal()); + dto.setOrgIdByCaPortal(entity.getOrgIdByCaPortal()); + dto.setApiIdByCaPortal(entity.getApiIdByCaPortal()); + dto.setPartnerCode(entity.getPartnerCode()); + dto.setApiIdByCa(entity.getApiIdByCa()); + return dto; + } + + /** + * timeslice를 ISO8601 형식 + UTC 시간대로 변환 + * 예: 2025-12-02T01:00:00+0000 + */ + private static String formatTimeslice(LocalDateTime timeslice) { + if (timeslice == null) return null; + return timeslice.format(ISO8601_FORMATTER); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java new file mode 100644 index 0000000..8543dec --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java @@ -0,0 +1,257 @@ +package com.eactive.eai.rms.onl.apim.obp; + +import com.eactive.apim.portal.obp.entity.ObpGwMetric; +import com.eactive.eai.rms.common.base.BaseService; +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceType; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.common.util.CommonUtil; +import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricDAO; +import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricService; +import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +/** + * ObpGwMetric Man Service - 비즈니스 로직 + * API Gateway 로그 데이터를 집계하여 ObpGwMetric 엔티티로 저장 + */ +@Service +@Transactional(transactionManager = "transactionManagerForEMS") +public class ObpGwMetricManService extends BaseService { + + private static final Logger log = LoggerFactory.getLogger(ObpGwMetricManService.class); + + /** + * 집계 범위 시간 (단위: 시간) + * - 1: 직전 1시간만 집계 + * - 5: 직전 5시간까지 각각 1시간 단위로 집계 + */ + private static final int AGGREGATION_HOUR_RANGE = 1; + + private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000"); + private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); + private static final DateTimeFormatter YYYYMMDD_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private final ObpGwMetricDAO obpGwMetricDAO; + private final ObpGwMetricService obpGwMetricService; + + public ObpGwMetricManService(ObpGwMetricDAO obpGwMetricDAO, ObpGwMetricService obpGwMetricService) { + this.obpGwMetricDAO = obpGwMetricDAO; + this.obpGwMetricService = obpGwMetricService; + } + + /** + * 다중 시간대 집계 실행 (Job에서 호출) + */ + public void executeHourlyAggregation() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime baseHour = now.truncatedTo(ChronoUnit.HOURS); // 현재 시간의 정각 + + log.info("========== ObpGwMetric 집계 시작 =========="); + log.info("실행 시간: {}, AGGREGATION_HOUR_RANGE: {}", now, AGGREGATION_HOUR_RANGE); + + for (int i = 1; i <= AGGREGATION_HOUR_RANGE; i++) { + LocalDateTime targetHour = baseHour.minusHours(i); + log.info("집계 대상 시간대: {}", targetHour); + createHourlyData(targetHour); + } + + log.info("========== ObpGwMetric 집계 완료 =========="); + } + + /** + * 1시간 단위 ObpGwMetric 데이터 생성 (UPSERT) + */ + public void createHourlyData(LocalDateTime targetHour) { + long jobStartTime = System.currentTimeMillis(); + log.info("ObpGwMetric 집계 시작: targetHour={}", targetHour); + + try { + String timeslice = targetHour.format(TIMESLICE_FORMATTER); + String yyyymmdd = targetHour.format(YYYYMMDD_FORMATTER); + String tableName = CommonUtil.getLogTable(yyyymmdd, true); + String startTime = targetHour.format(TIMESTAMP_FORMATTER); + String endTime = targetHour.plusHours(1).minusNanos(1).format(TIMESTAMP_FORMATTER); + + // ═══════════════════════════════════════════════════════════════════ + // 1단계: GW DB에서 EAIBZWKDSTCD 코드 목록 조회 + // ═══════════════════════════════════════════════════════════════════ + long step1Start = System.currentTimeMillis(); + DataSourceType apigwDataSourceType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW); + DataSourceContextHolder.setDataSourceType(apigwDataSourceType); + + List bzwkCodes = obpGwMetricDAO.selectAllBzwkCodes(apigwDataSourceType.getSchema()); + log.info("[1단계] EAIBZWKDSTCD 코드 조회 완료: count={}, elapsed={}ms", + bzwkCodes.size(), System.currentTimeMillis() - step1Start); + + // ═══════════════════════════════════════════════════════════════════ + // 2단계: 코드별 개별 쿼리 실행 후 결과 병합 + // ═══════════════════════════════════════════════════════════════════ + long step2Start = System.currentTimeMillis(); + Map aggregatedMap = new HashMap<>(); + int totalLogCount = 0; + + for (String bzwkCode : bzwkCodes) { + long codeStart = System.currentTimeMillis(); + + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", apigwDataSourceType.getSchema()); + paramMap.put("tableName", tableName); + paramMap.put("bzwkCode", bzwkCode); + paramMap.put("startTime", startTime); + paramMap.put("endTime", endTime); + paramMap.put("timeslice", timeslice); + + List partialResult = obpGwMetricDAO.selectHourlyMetricsByBzwkCode(paramMap); + + for (ObpGwMetricVO vo : partialResult) { + String key = vo.getAggregationKey(); + aggregatedMap.merge(key, vo, this::mergeMetricVO); + } + + if (!partialResult.isEmpty()) { + log.debug("[2단계] bzwkCode={}, 집계건수={}, elapsed={}ms", + bzwkCode, partialResult.size(), System.currentTimeMillis() - codeStart); + } + totalLogCount += partialResult.size(); + } + + List aggregatedList = new ArrayList<>(aggregatedMap.values()); + log.info("[2단계] 로그 집계 완료: 원본건수={}, 병합후건수={}, elapsed={}ms", + totalLogCount, aggregatedList.size(), System.currentTimeMillis() - step2Start); + + if (aggregatedList.isEmpty()) { + log.warn("집계 대상 데이터 없음: targetHour={}", targetHour); + return; + } + + // ═══════════════════════════════════════════════════════════════════ + // 3단계: 부가 정보 조회 (apiName, uri) - GW DB + // ═══════════════════════════════════════════════════════════════════ + long step3Start = System.currentTimeMillis(); + Set apiIds = aggregatedList.stream() + .map(ObpGwMetricVO::getApiId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map apiNameMap = obpGwMetricDAO.selectApiNames(apigwDataSourceType.getSchema(), apiIds); + Map uriMap = obpGwMetricDAO.selectUris(apigwDataSourceType.getSchema(), apiIds); + log.info("[3단계] 부가정보 조회 완료: apiCount={}, elapsed={}ms", + apiIds.size(), System.currentTimeMillis() - step3Start); + + // ═══════════════════════════════════════════════════════════════════ + // 4단계: 지표 DB로 전환 후 appId 조회 및 UPSERT + // ═══════════════════════════════════════════════════════════════════ + long step4Start = System.currentTimeMillis(); + DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING)); + + Set orgIds = aggregatedList.stream() + .map(ObpGwMetricVO::getOrgId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map appIdMap = obpGwMetricDAO.selectAppIdsByOrgIds(orgIds); + + int insertCount = 0; + int updateCount = 0; + + for (ObpGwMetricVO vo : aggregatedList) { + LocalDateTime timesliceDateTime = vo.getTimesliceAsLocalDateTime(); + + Optional existing = obpGwMetricService.findByUniqueKey( + timesliceDateTime, + vo.getApiId(), + vo.getClientId(), + vo.getHostname(), + vo.getOrgId() + ); + + if (existing.isPresent()) { + // UPDATE: 기존 데이터 갱신 + ObpGwMetric entity = existing.get(); + entity.setAttemptedCount(vo.getAttemptedCount()); + entity.setCompletedCount(vo.getCompletedCount()); + entity.setApiName(apiNameMap.get(vo.getApiId())); + entity.setUri(uriMap.get(vo.getApiId())); + entity.setAppId(appIdMap.get(vo.getOrgId())); + entity.setUpdateCount(entity.getUpdateCount() + 1); // 보정 횟수 증가 + obpGwMetricService.save(entity); + updateCount++; + } else { + // INSERT: 신규 데이터 생성 + ObpGwMetric entity = convertToEntity(vo, apiNameMap, uriMap, appIdMap); + obpGwMetricService.save(entity); + insertCount++; + } + } + + log.info("[4단계] UPSERT 완료: INSERT={}, UPDATE={}, elapsed={}ms", + insertCount, updateCount, System.currentTimeMillis() - step4Start); + + long totalElapsed = System.currentTimeMillis() - jobStartTime; + log.info("ObpGwMetric 집계 완료: targetHour={}, 총 소요시간={}ms ({}초)", + targetHour, totalElapsed, totalElapsed / 1000.0); + + } catch (Exception e) { + log.error("ObpGwMetric 집계 실패: targetHour={}", targetHour, e); + throw e; + } + } + + /** + * ObpGwMetricVO 병합 (count 누적) + */ + private ObpGwMetricVO mergeMetricVO(ObpGwMetricVO existing, ObpGwMetricVO incoming) { + existing.setAttemptedCount( + (existing.getAttemptedCount() != null ? existing.getAttemptedCount() : 0L) + + (incoming.getAttemptedCount() != null ? incoming.getAttemptedCount() : 0L) + ); + existing.setCompletedCount( + (existing.getCompletedCount() != null ? existing.getCompletedCount() : 0L) + + (incoming.getCompletedCount() != null ? incoming.getCompletedCount() : 0L) + ); + return existing; + } + + /** + * VO → Entity 변환 + */ + private ObpGwMetric convertToEntity(ObpGwMetricVO vo, Map apiNameMap, + Map uriMap, Map appIdMap) { + ObpGwMetric entity = new ObpGwMetric(); + entity.setClientId(vo.getClientId()); + entity.setHostname(vo.getHostname()); + entity.setTimeslice(vo.getTimesliceAsLocalDateTime()); + entity.setApiId(vo.getApiId()); + entity.setOrgId(vo.getOrgId()); + entity.setAttemptedCount(vo.getAttemptedCount()); + entity.setCompletedCount(vo.getCompletedCount()); + entity.setApiName(apiNameMap.get(vo.getApiId())); + entity.setUri(uriMap.get(vo.getApiId())); + entity.setAppId(appIdMap.get(vo.getOrgId())); + entity.setUpdateCount(0); + return entity; + } + + /** + * 특정 시간대의 GwMetric 데이터 조회 (REST API용) + */ + public List findByTimeslice(LocalDateTime timeslice) { + DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING)); + + List entities = obpGwMetricService.findByTimeslice(timeslice); + return entities.stream() + .map(ObpGwMetricDTO::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java b/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java new file mode 100644 index 0000000..35befd3 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java @@ -0,0 +1,56 @@ +package com.eactive.eai.rms.onl.common.service; + +import com.eactive.eai.rms.common.context.MonitoringContext; +import com.eactive.eai.rms.common.util.CommonUtil; +import com.eactive.eai.rms.onl.apim.obp.ObpGwMetricManService; +import com.eactive.eai.rms.onl.common.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.springframework.context.ApplicationContext; + +/** + * ObpGwMetric Hour Job - Quartz Job + * API Gateway 로그 데이터를 1시간 단위로 집계하여 ObpGwMetric 테이블에 저장 + * + *

Cron Schedule 권장:

+ *
    + *
  • 기본: 0 5 * * * ? (매시 05분 1회 실행)
  • + *
  • 보정 (15분): 0 5,20,35,50 * * * ? (15분 간격 4회 실행)
  • + *
  • 보정 (10분): 0 5,15,25,35,45,55 * * * ? (10분 간격 6회 실행)
  • + *
+ */ +public class ObpGwMetricHourJob implements Job { + + private static final Logger log = LoggerFactory.getLogger(ObpGwMetricHourJob.class); + + private transient MonitoringContext monitoringContext; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + log.info("*** START ObpGwMetricHourJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm")); + + ApplicationContext appContext = null; + try { + appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext"); + } catch (SchedulerException e) { + log.error("applicationContext get module error", e); + return; + } + + monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext"); + ObpGwMetricManService obpGwMetricManService = appContext.getBean(ObpGwMetricManService.class); + + try { + obpGwMetricManService.executeHourlyAggregation(); + } catch (Exception e) { + log.error("ObpGwMetricHourJob execution failed", e); + throw new JobExecutionException(e); + } + + log.info("*** END ObpGwMetricHourJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm")); + } +} diff --git a/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml new file mode 100644 index 0000000..caeb659 --- /dev/null +++ b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml @@ -0,0 +1,91 @@ + + + + + + + + SELECT EAIBZWKDSTCD + FROM $schemaId$.TSEAICM01 + ORDER BY EAIBZWKDSTCD + + + + + + + + + + + + + + + + SELECT + EAISVCNAME AS API_ID, + CLIENTID AS CLIENT_ID, + EAISEVRINSTNCNAME AS HOSTNAME, + ORGID AS ORG_ID, + '$timeslice$' AS TIMESLICE, + SUM(CASE WHEN LOGPRCSSSERNO = '100' THEN 1 ELSE 0 END) AS ATTEMPTED_COUNT, + SUM(CASE WHEN LOGPRCSSSERNO = '400' + AND (RSPNSERRCDNAME IS NULL + OR (RSPNSERRCDNAME NOT LIKE 'RE%' AND RSPNSERRCDNAME NOT LIKE 'RF%')) + THEN 1 ELSE 0 END) AS COMPLETED_COUNT + FROM $schemaId$.$tableName$ + WHERE EAIBZWKDSTCD = #bzwkCode# + AND MSGDPSTYMS BETWEEN #startTime# AND #endTime# + GROUP BY EAISVCNAME, CLIENTID, EAISEVRINSTNCNAME, ORGID + HAVING SUM(CASE WHEN LOGPRCSSSERNO = '100' THEN 1 ELSE 0 END) > 0 + OR SUM(CASE WHEN LOGPRCSSSERNO = '400' THEN 1 ELSE 0 END) > 0 + + + + + SELECT + EAISVCNAME AS API_ID, + EAISVCDESC AS API_NAME + FROM $schemaId$.TSEAIHE01 + WHERE EAISVCNAME IN + + #apiIds[]# + + + + + + SELECT + EAISVCNAME AS API_ID, + BZWKSVCKEYNAME AS URI + FROM $schemaId$.TSEAIHS04 + WHERE EAISVCNAME IN + + #apiIds[]# + + + + + + SELECT + ORG_ID, + ID AS APP_ID + FROM PTL_APP_REQUEST + WHERE ORG_ID IN + + #orgIds[]# + + + +