diff --git a/WebContent/WEB-INF/applicationContext-jpa.xml b/WebContent/WEB-INF/applicationContext-jpa.xml index 53b9b53..b1da502 100644 --- a/WebContent/WEB-INF/applicationContext-jpa.xml +++ b/WebContent/WEB-INF/applicationContext-jpa.xml @@ -68,6 +68,8 @@ + + diff --git a/WebContent/WEB-INF/springapp-servlet.xml b/WebContent/WEB-INF/springapp-servlet.xml index 3db1b50..3bef9a1 100644 --- a/WebContent/WEB-INF/springapp-servlet.xml +++ b/WebContent/WEB-INF/springapp-servlet.xml @@ -150,4 +150,6 @@ + + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp new file mode 100644 index 0000000..0169457 --- /dev/null +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp @@ -0,0 +1,356 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> +<%@ page import="java.io.*"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + request.setCharacterEncoding("UTF-8"); +%> + + + API 시간별 통계 + + + + + + + + +
+
+ +
+
+
+ +
+
API 시간별 통계
+ + + + + + + + + + + + + + + + + + + + + + + + + +
조회일자 + + (선택일 00시 ~ 23시 조회) +
API명 + + 인스턴스 + +
업무구분 + + 클라이언트ID + +
Inbound Adapter + + Outbound Adapter + +
+
+
+
+
+
+
+
+
+ + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp new file mode 100644 index 0000000..7013212 --- /dev/null +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp @@ -0,0 +1,421 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> +<%@ page import="java.io.*"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + request.setCharacterEncoding("UTF-8"); +%> + + + API 분단위 통계 + + + + + + + + +
+
+ +
+
+
+ +
+
API 분단위 통계
+ + + + + + + + + + + + + + + + + + + + + + + + + +
조회기간 + + + ~ + + + (최대 1시간, 초과시 자동 조정) +
API명 + + 인스턴스 + +
업무구분 + + 클라이언트ID + +
Inbound Adapter + + Outbound Adapter + +
+
+
+
+
+
+
+
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHour.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHour.java new file mode 100644 index 0000000..e12d5af --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHour.java @@ -0,0 +1,93 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.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 lombok.Getter; +import lombok.Setter; + +/** + * API Gateway 시간별 처리 통계 + */ +@Getter +@Setter +@Entity +@Table(name = "API_STATS_HOUR") +@IdClass(ApiStatsHourId.class) +public class ApiStatsHour implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STAT_TIME", nullable = false) + private LocalDateTime statTime; + + @Id + @Column(name = "API_NAME", nullable = false, length = 100) + private String apiName; + + @Id + @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50) + private String gwInstanceId; + + @Id + @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50) + private String bizDivCode = "NONE"; + + @Id + @Column(name = "CLIENT_ID", nullable = false, length = 100) + private String clientId = "NONE"; + + @Id + @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100) + private String inboundAdapter = "NONE"; + + @Id + @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100) + private String outboundAdapter = "NONE"; + + @Column(name = "TOTAL_CNT", nullable = false) + private Long totalCnt = 0L; + + @Column(name = "SUCCESS_CNT", nullable = false) + private Long successCnt = 0L; + + @Column(name = "TIMEOUT_CNT", nullable = false) + private Long timeoutCnt = 0L; + + @Column(name = "SYSTEM_ERR_CNT", nullable = false) + private Long systemErrCnt = 0L; + + @Column(name = "BIZ_ERR_CNT", nullable = false) + private Long bizErrCnt = 0L; + + @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false) + private Long seq900TimeoutCnt = 0L; + + @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false) + private Long seq900SystemErrCnt = 0L; + + @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false) + private Long seq900BizErrCnt = 0L; + + @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3) + private BigDecimal avgRespTime = BigDecimal.ZERO; + + @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3) + private BigDecimal minRespTime = BigDecimal.ZERO; + + @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3) + private BigDecimal maxRespTime = BigDecimal.ZERO; + + @Column(name = "P50_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p50RespTime = BigDecimal.ZERO; + + @Column(name = "P95_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p95RespTime = BigDecimal.ZERO; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourId.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourId.java new file mode 100644 index 0000000..d6c5ded --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourId.java @@ -0,0 +1,26 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.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; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourRepository.java new file mode 100644 index 0000000..f745db2 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourRepository.java @@ -0,0 +1,7 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; + +interface ApiStatsHourRepository extends BaseRepository { + +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java new file mode 100644 index 0000000..714e783 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java @@ -0,0 +1,133 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.data.jpa.AbstractDataService; +import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; + +@Service +@Transactional +public class ApiStatsHourService + extends AbstractDataService { + + private static final int MAX_HOURS = 24; + + public Page selectList(ApiStatsSearch search, Pageable pageable) { + QApiStatsHour q = QApiStatsHour.apiStatsHour; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartDateTime() != null) { + builder.and(q.statTime.goe(search.getParsedStartDateTime())); + builder.and(q.statTime.loe(search.getParsedEndDateTime())); + } + + if (StringUtils.isNotBlank(search.getSearchApiName())) { + builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName())); + } + + if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) { + builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId())); + } + + if (StringUtils.isNotBlank(search.getSearchBizDivCode())) { + builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode())); + } + + if (StringUtils.isNotBlank(search.getSearchClientId())) { + builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId())); + } + + if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) { + builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter())); + } + + if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) { + builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter())); + } + + return repository.findAll(builder, pageable); + } + + public List selectChartData(ApiStatsSearch search) { + QApiStatsHour q = QApiStatsHour.apiStatsHour; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartDateTime() != null) { + builder.and(q.statTime.goe(search.getParsedStartDateTime())); + builder.and(q.statTime.loe(search.getParsedEndDateTime())); + } + + if (StringUtils.isNotBlank(search.getSearchApiName())) { + builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName())); + } + + if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) { + builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId())); + } + + if (StringUtils.isNotBlank(search.getSearchBizDivCode())) { + builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode())); + } + + if (StringUtils.isNotBlank(search.getSearchClientId())) { + builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId())); + } + + return getJPAQueryFactory() + .select(Projections.constructor(ApiStatsChartUI.class, + q.statTime, + q.totalCnt.sum(), + q.successCnt.sum(), + q.timeoutCnt.sum(), + q.systemErrCnt.sum(), + q.bizErrCnt.sum(), + q.avgRespTime.avg(), + q.p50RespTime.avg(), + q.p95RespTime.avg())) + .from(q) + .where(builder) + .groupBy(q.statTime) + .orderBy(q.statTime.asc()) + .fetch(); + } + + /** + * 조회 기간 계산 (최대 24시간 제한) + */ + private void calculateDateRange(ApiStatsSearch search) { + if (StringUtils.isBlank(search.getSearchStartDateTime())) { + return; + } + + LocalDateTime startDateTime = LocalDateTime.parse(search.getSearchStartDateTime() + "00", + DateTimeFormatter.ofPattern("yyyyMMddHHmm")); + LocalDateTime endDateTime; + + if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { + endDateTime = LocalDateTime.parse(search.getSearchEndDateTime() + "00", + DateTimeFormatter.ofPattern("yyyyMMddHHmm")); + // 24시간 초과 시 시작시간 기준 24시간 후로 강제 설정 + if (java.time.Duration.between(startDateTime, endDateTime).toHours() > MAX_HOURS) { + endDateTime = startDateTime.plusHours(MAX_HOURS); + } + } else { + endDateTime = startDateTime.plusHours(MAX_HOURS); + } + + search.setParsedStartDateTime(startDateTime); + search.setParsedEndDateTime(endDateTime); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinute.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinute.java new file mode 100644 index 0000000..78ce5e8 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinute.java @@ -0,0 +1,93 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.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 lombok.Getter; +import lombok.Setter; + +/** + * API Gateway 분단위 처리 통계 + */ +@Getter +@Setter +@Entity +@Table(name = "API_STATS_MINUTE") +@IdClass(ApiStatsMinuteId.class) +public class ApiStatsMinute implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STAT_TIME", nullable = false) + private LocalDateTime statTime; + + @Id + @Column(name = "API_NAME", nullable = false, length = 100) + private String apiName; + + @Id + @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50) + private String gwInstanceId; + + @Id + @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50) + private String bizDivCode = "NONE"; + + @Id + @Column(name = "CLIENT_ID", nullable = false, length = 100) + private String clientId = "NONE"; + + @Id + @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100) + private String inboundAdapter = "NONE"; + + @Id + @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100) + private String outboundAdapter = "NONE"; + + @Column(name = "TOTAL_CNT", nullable = false) + private Long totalCnt = 0L; + + @Column(name = "SUCCESS_CNT", nullable = false) + private Long successCnt = 0L; + + @Column(name = "TIMEOUT_CNT", nullable = false) + private Long timeoutCnt = 0L; + + @Column(name = "SYSTEM_ERR_CNT", nullable = false) + private Long systemErrCnt = 0L; + + @Column(name = "BIZ_ERR_CNT", nullable = false) + private Long bizErrCnt = 0L; + + @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false) + private Long seq900TimeoutCnt = 0L; + + @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false) + private Long seq900SystemErrCnt = 0L; + + @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false) + private Long seq900BizErrCnt = 0L; + + @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3) + private BigDecimal avgRespTime = BigDecimal.ZERO; + + @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3) + private BigDecimal minRespTime = BigDecimal.ZERO; + + @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3) + private BigDecimal maxRespTime = BigDecimal.ZERO; + + @Column(name = "P50_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p50RespTime = BigDecimal.ZERO; + + @Column(name = "P95_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p95RespTime = BigDecimal.ZERO; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteId.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteId.java new file mode 100644 index 0000000..45671fe --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteId.java @@ -0,0 +1,26 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.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; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteRepository.java new file mode 100644 index 0000000..85d9091 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteRepository.java @@ -0,0 +1,7 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; + +interface ApiStatsMinuteRepository extends BaseRepository { + +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java new file mode 100644 index 0000000..4570b34 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java @@ -0,0 +1,133 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.data.jpa.AbstractDataService; +import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; + +@Service +@Transactional +public class ApiStatsMinuteService + extends AbstractDataService { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); + + private static final int MAX_MINUTES = 60; + + public Page selectList(ApiStatsSearch search, Pageable pageable) { + QApiStatsMinute q = QApiStatsMinute.apiStatsMinute; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartDateTime() != null) { + builder.and(q.statTime.goe(search.getParsedStartDateTime())); + builder.and(q.statTime.loe(search.getParsedEndDateTime())); + } + + if (StringUtils.isNotBlank(search.getSearchApiName())) { + builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName())); + } + + if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) { + builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId())); + } + + if (StringUtils.isNotBlank(search.getSearchBizDivCode())) { + builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode())); + } + + if (StringUtils.isNotBlank(search.getSearchClientId())) { + builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId())); + } + + if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) { + builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter())); + } + + if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) { + builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter())); + } + + return repository.findAll(builder, pageable); + } + + public List selectChartData(ApiStatsSearch search) { + QApiStatsMinute q = QApiStatsMinute.apiStatsMinute; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartDateTime() != null) { + builder.and(q.statTime.goe(search.getParsedStartDateTime())); + builder.and(q.statTime.loe(search.getParsedEndDateTime())); + } + + if (StringUtils.isNotBlank(search.getSearchApiName())) { + builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName())); + } + + if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) { + builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId())); + } + + if (StringUtils.isNotBlank(search.getSearchBizDivCode())) { + builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode())); + } + + if (StringUtils.isNotBlank(search.getSearchClientId())) { + builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId())); + } + + return getJPAQueryFactory() + .select(Projections.constructor(ApiStatsChartUI.class, + q.statTime, + q.totalCnt.sum(), + q.successCnt.sum(), + q.timeoutCnt.sum(), + q.systemErrCnt.sum(), + q.bizErrCnt.sum(), + q.avgRespTime.avg(), + q.p50RespTime.avg(), + q.p95RespTime.avg())) + .from(q) + .where(builder) + .groupBy(q.statTime) + .orderBy(q.statTime.asc()) + .fetch(); + } + + /** + * 조회 기간 계산 (최대 1시간 제한) + */ + private void calculateDateRange(ApiStatsSearch search) { + if (StringUtils.isBlank(search.getSearchStartDateTime())) { + return; + } + + LocalDateTime startDateTime = LocalDateTime.parse(search.getSearchStartDateTime(), DATE_TIME_FORMATTER); + LocalDateTime endDateTime; + + if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { + endDateTime = LocalDateTime.parse(search.getSearchEndDateTime(), DATE_TIME_FORMATTER); + // 1시간(60분) 초과 시 시작시간 기준 1시간 후로 강제 설정 + if (java.time.Duration.between(startDateTime, endDateTime).toMinutes() > MAX_MINUTES) { + endDateTime = startDateTime.plusMinutes(MAX_MINUTES); + } + } else { + endDateTime = startDateTime.plusMinutes(MAX_MINUTES); + } + + search.setParsedStartDateTime(startDateTime); + search.setParsedEndDateTime(endDateTime); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/package-info.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/package-info.java new file mode 100644 index 0000000..6dda3ec --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/package-info.java @@ -0,0 +1 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; \ No newline at end of file diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java new file mode 100644 index 0000000..1b11f19 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java @@ -0,0 +1,56 @@ +package com.eactive.ext.kjb.statistics; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; + +import com.eactive.eai.rms.common.vo.GridResponse; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService; +import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; +import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiStatsHourController { + + private final ApiStatsHourService service; + private final ApiStatsUIMapper mapper; + + @GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view") + public String view() { + return "/onl/kjb/statistics/apiStatsHourMan"; + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=LIST") + public ResponseEntity> selectList(ApiStatsSearch search, Pageable pageable) { + Pageable sortedPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + Sort.by(Sort.Order.desc("statTime"))); + + Page page = service.selectList(search, sortedPageable); + Page uiPage = page.map(mapper::toVo); + + return ResponseEntity.ok(new GridResponse<>(uiPage)); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=CHART") + public ResponseEntity> selectChartData(ApiStatsSearch search) { + List chartData = service.selectChartData(search); + return ResponseEntity.ok(chartData); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java new file mode 100644 index 0000000..a1b9021 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java @@ -0,0 +1,56 @@ +package com.eactive.ext.kjb.statistics; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; + +import com.eactive.eai.rms.common.vo.GridResponse; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService; +import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; +import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiStatsMinuteController { + + private final ApiStatsMinuteService service; + private final ApiStatsUIMapper mapper; + + @GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view") + public String view() { + return "/onl/kjb/statistics/apiStatsMinuteMan"; + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=LIST") + public ResponseEntity> selectList(ApiStatsSearch search, Pageable pageable) { + Pageable sortedPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + Sort.by(Sort.Order.desc("statTime"))); + + Page page = service.selectList(search, sortedPageable); + Page uiPage = page.map(mapper::toVo); + + return ResponseEntity.ok(new GridResponse<>(uiPage)); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=CHART") + public ResponseEntity> selectChartData(ApiStatsSearch search) { + List chartData = service.selectChartData(search); + return ResponseEntity.ok(chartData); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/README.md b/src/main/java/com/eactive/ext/kjb/statistics/README.md new file mode 100644 index 0000000..3a93ba4 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/README.md @@ -0,0 +1,102 @@ +# API Gateway 통계 화면 + +## 개요 + +API Gateway의 분단위/시간별 처리 통계를 조회하는 관리 화면입니다. + +## 파일 구조 + +``` +com.eactive.ext.kjb.statistics/ +├── ApiStatsMinuteController.java # 분단위 통계 Controller +├── ApiStatsMinuteService.java # 분단위 통계 Service +├── ApiStatsHourController.java # 시간별 통계 Controller +├── ApiStatsHourService.java # 시간별 통계 Service +├── entity/ +│ ├── ApiStatsMinute.java # 분단위 통계 Entity +│ ├── ApiStatsMinuteId.java # 분단위 통계 복합키 +│ ├── ApiStatsHour.java # 시간별 통계 Entity +│ └── ApiStatsHourId.java # 시간별 통계 복합키 +├── repository/ +│ ├── ApiStatsMinuteRepository.java +│ └── ApiStatsHourRepository.java +├── ui/ +│ ├── ApiStatsUI.java # 통계 UI DTO +│ └── ApiStatsSearch.java # 검색조건 DTO +├── mapping/ +│ └── ApiStatsUIMapper.java # Entity ↔ UI 매핑 +└── README.md + +JSP 화면: +WebContent/jsp/kjb/statistics/ +├── apiStatsMinuteMan.jsp # 분단위 통계 화면 +└── apiStatsHourMan.jsp # 시간별 통계 화면 +``` + +## URL + +| 화면 | URL | +|------|-----| +| 분단위 통계 | `/kjb/statistics/apiStatsMinuteMan.view` | +| 시간별 통계 | `/kjb/statistics/apiStatsHourMan.view` | + +## 테이블 + +| 테이블명 | 설명 | +|----------|------| +| API_STATS_MINUTE | 분단위 통계 | +| API_STATS_HOUR | 시간별 통계 | + +## 복합키 구성 + +- statTime (통계 시간) +- apiName (API명) +- gwInstanceId (Gateway 인스턴스 ID) +- bizDivCode (업무구분코드) +- clientId (클라이언트 ID) +- inboundAdapter (Inbound Adapter) +- outboundAdapter (Outbound Adapter) + +## 메트릭 + +### 건수 메트릭 +- totalCnt: 총 처리 건수 +- successCnt: 성공 건수 +- timeoutCnt: Timeout 오류 건수 +- systemErrCnt: 시스템 오류 건수 +- bizErrCnt: 업무 오류 건수 +- seq900TimeoutCnt: 로그시퀀스900 Timeout 건수 +- seq900SystemErrCnt: 로그시퀀스900 시스템 오류 건수 +- seq900BizErrCnt: 로그시퀀스900 업무 오류 건수 + +### 응답시간 메트릭 (ms) +- avgRespTime: 평균 응답시간 +- minRespTime: 최소 응답시간 +- maxRespTime: 최대 응답시간 +- p50RespTime: 50 백분위 응답시간 +- p95RespTime: 95 백분위 응답시간 + +## Multi-tenancy + +API Gateway 통계는 `APIGW` 스키마에 저장됩니다. +Controller에서 `DataSourceContextHolder`를 사용하여 스키마를 전환합니다. + +```java +DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); +``` + +## 검색 조건 + +- 조회기간 (시작~종료) +- API명 (like 검색) +- Gateway 인스턴스 ID +- 업무구분코드 (like 검색) +- 클라이언트 ID (like 검색) + +## 기술 스택 + +- JPA + QueryDSL +- MapStruct (Entity ↔ UI 매핑) +- Spring MVC (ResponseEntity) +- jqGrid (목록 화면) diff --git a/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java b/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java new file mode 100644 index 0000000..59b241b --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java @@ -0,0 +1,42 @@ +package com.eactive.ext.kjb.statistics.mapping; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import com.eactive.eai.data.mapper.BaseMapperConfig; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute; +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +@Mapper(config = BaseMapperConfig.class) +public interface ApiStatsUIMapper { + + DateTimeFormatter MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + DateTimeFormatter HOUR_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00"); + + @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatMinuteTime") + ApiStatsUI toVo(ApiStatsMinute entity); + + @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatHourTime") + ApiStatsUI toVo(ApiStatsHour entity); + + @Named("formatMinuteTime") + default String formatMinuteTime(LocalDateTime dateTime) { + if (dateTime == null) { + return null; + } + return dateTime.format(MINUTE_FORMATTER); + } + + @Named("formatHourTime") + default String formatHourTime(LocalDateTime dateTime) { + if (dateTime == null) { + return null; + } + return dateTime.format(HOUR_FORMATTER); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/package-info.java b/src/main/java/com/eactive/ext/kjb/statistics/package-info.java new file mode 100644 index 0000000..b70882e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/package-info.java @@ -0,0 +1 @@ +package com.eactive.ext.kjb.statistics; \ No newline at end of file diff --git a/src/main/java/com/eactive/ext/kjb/statistics/통계테이블_설계.md b/src/main/java/com/eactive/ext/kjb/statistics/statistics_table.md similarity index 100% rename from src/main/java/com/eactive/ext/kjb/statistics/통계테이블_설계.md rename to src/main/java/com/eactive/ext/kjb/statistics/statistics_table.md diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java new file mode 100644 index 0000000..d2e7c7c --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java @@ -0,0 +1,44 @@ +package com.eactive.ext.kjb.statistics.ui; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * API 통계 차트 데이터 DTO (시간대별 집계) + */ +@Data +@NoArgsConstructor +public class ApiStatsChartUI { + private String statTime; + + // 건수 메트릭 (SUM) + private Long totalCnt; + private Long successCnt; + private Long timeoutCnt; + private Long systemErrCnt; + private Long bizErrCnt; + + // 응답시간 메트릭 (AVG) + private BigDecimal avgRespTime; + private BigDecimal p50RespTime; + private BigDecimal p95RespTime; + + // QueryDSL Projections용 생성자 + public ApiStatsChartUI(LocalDateTime statTime, Long totalCnt, Long successCnt, + Long timeoutCnt, Long systemErrCnt, Long bizErrCnt, + Double avgRespTime, Double p50RespTime, Double p95RespTime) { + this.statTime = statTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")); + this.totalCnt = totalCnt != null ? totalCnt : 0L; + this.successCnt = successCnt != null ? successCnt : 0L; + this.timeoutCnt = timeoutCnt != null ? timeoutCnt : 0L; + this.systemErrCnt = systemErrCnt != null ? systemErrCnt : 0L; + this.bizErrCnt = bizErrCnt != null ? bizErrCnt : 0L; + this.avgRespTime = avgRespTime != null ? BigDecimal.valueOf(avgRespTime) : BigDecimal.ZERO; + this.p50RespTime = p50RespTime != null ? BigDecimal.valueOf(p50RespTime) : BigDecimal.ZERO; + this.p95RespTime = p95RespTime != null ? BigDecimal.valueOf(p95RespTime) : BigDecimal.ZERO; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java new file mode 100644 index 0000000..68c75cd --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java @@ -0,0 +1,25 @@ +package com.eactive.ext.kjb.statistics.ui; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * API 통계 검색조건 DTO + */ +@Data +public class ApiStatsSearch { + private String searchStartDateTime; + private String searchEndDateTime; + private String searchApiName; + private String searchGwInstanceId; + private String searchBizDivCode; + private String searchClientId; + private String searchInboundAdapter; + private String searchOutboundAdapter; + + /** 파싱된 시작시간 */ + private LocalDateTime parsedStartDateTime; + /** 파싱된 종료시간 */ + private LocalDateTime parsedEndDateTime; +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java new file mode 100644 index 0000000..19e0368 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java @@ -0,0 +1,36 @@ +package com.eactive.ext.kjb.statistics.ui; + +import java.math.BigDecimal; + +import lombok.Data; + +/** + * API 통계 UI DTO (분단위/시간별 공통) + */ +@Data +public class ApiStatsUI { + private String statTime; + private String apiName; + private String gwInstanceId; + private String bizDivCode; + private String clientId; + private String inboundAdapter; + private String outboundAdapter; + + // 건수 메트릭 + private Long totalCnt; + private Long successCnt; + private Long timeoutCnt; + private Long systemErrCnt; + private Long bizErrCnt; + private Long seq900TimeoutCnt; + private Long seq900SystemErrCnt; + private Long seq900BizErrCnt; + + // 응답시간 메트릭 + private BigDecimal avgRespTime; + private BigDecimal minRespTime; + private BigDecimal maxRespTime; + private BigDecimal p50RespTime; + private BigDecimal p95RespTime; +}