API Status 페이지 기능 및 UI 업데이트:
- 상태 모니터링 시각 및 세부 정보 노출 관련 로직 추가 - 지연 필터 및 상대 시간 표기 기능 구현 - 점검/장애 필터 옵션 및 탐지 메타 정보 조회 로직 보완
This commit is contained in:
@@ -11,8 +11,8 @@ import java.time.LocalDateTime;
|
||||
/**
|
||||
* API 상태 모니터링 결과 (AGWAPP.API_STATUS).
|
||||
*
|
||||
* <p>eapim-admin 의 {@code ApiStatusMonitorJob}(Quartz, 매분)이 상태 변화가 있을 때만 upsert 한다.
|
||||
* 포털은 읽기 전용으로 "마지막 상태 갱신 시각" 표시에 사용한다.</p>
|
||||
* <p>eapim-admin 의 {@code ApiStatusMonitorJob} 이 상태 변화가 있을 때만 upsert 한다.
|
||||
* 포털은 읽기 전용으로 "마지막 상태 변경 시각" 표시에 사용한다.</p>
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "API_STATUS")
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import java.util.Optional;
|
||||
public interface GwApiStatusRepository extends Repository<GwApiStatus, String> {
|
||||
|
||||
/**
|
||||
* 탐지 결과가 마지막으로 갱신된 시각. 데이터가 없으면 empty.
|
||||
* API 상태가 마지막으로 변경된 시각. 데이터가 없으면 empty.
|
||||
*/
|
||||
@Query("SELECT MAX(s.modifiedDate) FROM GwApiStatus s")
|
||||
Optional<LocalDateTime> findLastModifiedDate();
|
||||
|
||||
+6
-1
@@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -62,7 +63,11 @@ public class ApiStatusController {
|
||||
ModelAndView mav = new ModelAndView("djb/apistatus/index");
|
||||
mav.addObject("windowDays", ApiStatusSupport.DEFAULT_WINDOW_DAYS);
|
||||
mav.addObject("authenticated", SecurityUtil.isAuthenticated());
|
||||
mav.addObject("lastStatusUpdatedAt", catalogService.getLastStatusUpdatedAt());
|
||||
LocalDateTime lastFireAt = catalogService.getLastMonitorFireAt();
|
||||
mav.addObject("lastFireAt", lastFireAt);
|
||||
mav.addObject("lastFireRelative",
|
||||
ApiStatusSupport.relativeTime(lastFireAt, ApiStatusSupport.now()));
|
||||
mav.addObject("lastStatusChangedAt", catalogService.getLastStatusChangedAt());
|
||||
return mav;
|
||||
}
|
||||
|
||||
|
||||
+59
-6
@@ -5,12 +5,16 @@ import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
import com.eactive.apim.portal.djb.apistatus.dto.ApiOptionDTO;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -18,7 +22,7 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* API Status 화면의 부가 조회 - 필터용 API 목록, 탐지 결과 갱신 시각.
|
||||
* API Status 화면의 부가 조회 - 필터용 API 목록, 상태 모니터링 최근 실행 시각.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -26,9 +30,21 @@ import java.util.stream.Collectors;
|
||||
@Transactional(readOnly = true)
|
||||
public class ApiStatusCatalogService {
|
||||
|
||||
public static final String PROPERTY_GROUP = "Portal";
|
||||
|
||||
/** API 상태 모니터링 Quartz Job 이름 (eapim-admin QRTZ_JOB_DETAILS.JOB_NAME) */
|
||||
public static final String KEY_MONITOR_JOB_NAME = "djb.apistatus.monitor.job-name";
|
||||
|
||||
/** 실서버(DAPM) QRTZ_JOB_DETAILS 실사값 (2026-07-31) */
|
||||
private static final String DEFAULT_MONITOR_JOB_NAME = "ApiStatusMonitorJob";
|
||||
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
private final GwApiStatusRepository gwApiStatusRepository;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
/**
|
||||
* 이슈 이력 필터용 API 목록.
|
||||
*
|
||||
@@ -54,15 +70,52 @@ public class ApiStatusCatalogService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 탐지 결과(AGWAPP.API_STATUS)가 마지막으로 갱신된 시각.
|
||||
* 상태 변화가 있을 때만 갱신되므로 "마지막 상태 변경 시각" 이다.
|
||||
* API 상태 모니터링 Job(eapim-admin Quartz)의 마지막 실행 시각.
|
||||
*
|
||||
* <p>Job 이름은 PTL_PROPERTY({@code Portal} / {@code djb.apistatus.monitor.job-name})로
|
||||
* 바꿀 수 있으며, 최초 접근 시 기본값으로 row 가 자동 생성된다.
|
||||
* Quartz 메타(QRTZ_TRIGGERS)는 EMSAPP 스키마라 EMS EntityManager 로 직접 읽는다.</p>
|
||||
*
|
||||
* <p>readOnly 를 풀어야 최초 접근 시 프로퍼티 row 자동 생성(INSERT)이 가능하다.</p>
|
||||
*/
|
||||
public LocalDateTime getLastStatusUpdatedAt() {
|
||||
@Transactional
|
||||
public LocalDateTime getLastMonitorFireAt() {
|
||||
try {
|
||||
String jobName = portalPropertyService.getOrCreateProperty(
|
||||
PROPERTY_GROUP, KEY_MONITOR_JOB_NAME, DEFAULT_MONITOR_JOB_NAME,
|
||||
"API 상태 모니터링 Quartz Job 이름 (eapim-admin QRTZ_JOB_DETAILS.JOB_NAME)");
|
||||
if (StringUtils.isBlank(jobName)) {
|
||||
jobName = DEFAULT_MONITOR_JOB_NAME;
|
||||
}
|
||||
|
||||
Object fireTime = entityManager.createNativeQuery(
|
||||
"SELECT MAX(PREV_FIRE_TIME) FROM QRTZ_TRIGGERS WHERE JOB_NAME = :jobName")
|
||||
.setParameter("jobName", jobName)
|
||||
.getSingleResult();
|
||||
if (!(fireTime instanceof Number)) {
|
||||
return null;
|
||||
}
|
||||
long epochMillis = ((Number) fireTime).longValue();
|
||||
if (epochMillis <= 0) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ApiStatusSupport.ZONE);
|
||||
} catch (Exception e) {
|
||||
// 스케줄 메타 조회 실패가 화면 전체를 막지 않도록 한다
|
||||
log.warn("API 상태 모니터링 실행 시각 조회 실패", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 상태가 마지막으로 <b>변경</b>된 시각 (AGWAPP.API_STATUS 최근 upsert).
|
||||
* 실행 시각(fire)과 달리 상태 변화가 있을 때만 갱신된다.
|
||||
*/
|
||||
public LocalDateTime getLastStatusChangedAt() {
|
||||
try {
|
||||
return gwApiStatusRepository.findLastModifiedDate().orElse(null);
|
||||
} catch (Exception e) {
|
||||
// 게이트웨이 조회 실패가 화면 전체를 막지 않도록 한다
|
||||
log.warn("API 상태 갱신 시각 조회 실패", e);
|
||||
log.warn("API 상태 변경 시각 조회 실패", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-7
@@ -119,32 +119,49 @@ public class ApiStatusIssueHistoryService {
|
||||
return new PageImpl<>(content, pageable, incidents.size());
|
||||
}
|
||||
|
||||
/** 자동 탐지 지연 건의 INTERFACE_ID prefix (admin ApiStatusDetectionService 가 event 명으로 생성) */
|
||||
private static final String DELAY_INTERFACE_PREFIX = "DELAY_START:";
|
||||
|
||||
private List<DjbApistatusIncident> findOverlapping(LocalDateTime from, LocalDateTime to,
|
||||
String apiId, String kind) {
|
||||
List<DjbApistatusIncident> incidents = StringUtils.isBlank(apiId)
|
||||
? incidentQueryRepository.findVisibleOverlapping(from, to)
|
||||
: incidentQueryRepository.findVisibleOverlappingByApi(from, to, apiId);
|
||||
|
||||
IncidentKind selected = parseKind(kind);
|
||||
if (selected == null) {
|
||||
java.util.function.Predicate<DjbApistatusIncident> filter = kindFilter(kind);
|
||||
if (filter == null) {
|
||||
return incidents;
|
||||
}
|
||||
return incidents.stream()
|
||||
.filter(incident -> incident.getKind() == selected)
|
||||
.collect(Collectors.toList());
|
||||
return incidents.stream().filter(filter).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 유형 필터. 미지정/ALL/알 수 없는 값은 전체(null)로 본다.
|
||||
*
|
||||
* <p>지연(DELAY)은 별도 KIND 가 아니라 자동 탐지 시 INCIDENT 로 흡수된다(ADR-F12).
|
||||
* 자동 탐지 지연 건은 INTERFACE_ID 가 {@code DELAY_START:} 로 시작하므로 이를 기준으로 가른다.
|
||||
* "장애만" 은 지연을 제외한 INCIDENT 다.</p>
|
||||
*/
|
||||
private IncidentKind parseKind(String kind) {
|
||||
private java.util.function.Predicate<DjbApistatusIncident> kindFilter(String kind) {
|
||||
if (StringUtils.isBlank(kind) || "ALL".equalsIgnoreCase(kind)) {
|
||||
return null;
|
||||
}
|
||||
if ("DELAY".equalsIgnoreCase(kind)) {
|
||||
return incident -> incident.getKind() == IncidentKind.INCIDENT && isDelay(incident);
|
||||
}
|
||||
if ("INCIDENT".equalsIgnoreCase(kind)) {
|
||||
return incident -> incident.getKind() == IncidentKind.INCIDENT && !isDelay(incident);
|
||||
}
|
||||
try {
|
||||
return IncidentKind.valueOf(kind.toUpperCase());
|
||||
IncidentKind selected = IncidentKind.valueOf(kind.toUpperCase());
|
||||
return incident -> incident.getKind() == selected;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDelay(DjbApistatusIncident incident) {
|
||||
return incident.getInterfaceId() != null
|
||||
&& incident.getInterfaceId().startsWith(DELAY_INTERFACE_PREFIX);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,4 +72,26 @@ public final class ApiStatusSupport {
|
||||
}
|
||||
return Duration.between(from, to).toMinutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* "N분 전" 형태의 상대 시간 표기.
|
||||
*/
|
||||
public static String relativeTime(LocalDateTime past, LocalDateTime now) {
|
||||
if (past == null || now == null) {
|
||||
return null;
|
||||
}
|
||||
long minutes = Duration.between(past, now).toMinutes();
|
||||
if (minutes < 1) {
|
||||
return "방금 전";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return minutes + "분 전";
|
||||
}
|
||||
long hours = minutes / 60;
|
||||
if (hours < 24) {
|
||||
long restMinutes = minutes % 60;
|
||||
return restMinutes == 0 ? hours + "시간 전" : hours + "시간 " + restMinutes + "분 전";
|
||||
}
|
||||
return (hours / 24) + "일 전";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26711,6 +26711,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
--as-radius: 12px;
|
||||
--as-pill: 999px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
padding: 0 0 70px;
|
||||
}
|
||||
@@ -27488,7 +27489,13 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.api-status .as-index-bar {
|
||||
height: 36px;
|
||||
height: 40px;
|
||||
}
|
||||
.api-status .as-index-bar .as-index-cell {
|
||||
flex: 0 0 12px;
|
||||
}
|
||||
.api-status .as-uptime .as-bar-chart .as-bar {
|
||||
flex: 0 0 12px;
|
||||
}
|
||||
.api-status .as-issue-card {
|
||||
padding: 18px 16px;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -10,13 +10,16 @@
|
||||
<div class="as-title-banner">
|
||||
<div class="as-title-banner-inner">
|
||||
<h1>API Status</h1>
|
||||
<div class="as-title-meta" th:if="${lastStatusUpdatedAt != null}">
|
||||
<p class="as-title-meta-main">
|
||||
<div class="as-title-meta" th:if="${lastFireAt != null or lastStatusChangedAt != null}">
|
||||
<p class="as-title-meta-main" th:if="${lastFireAt != null}">
|
||||
<span class="as-title-dot"></span>
|
||||
<span>마지막 상태 갱신 </span>
|
||||
<strong th:text="${#temporals.format(lastStatusUpdatedAt, 'yyyy-MM-dd HH:mm')} + ' KST'">-</strong>
|
||||
<span>마지막 점검 </span>
|
||||
<strong th:text="${lastFireRelative}">-</strong>
|
||||
</p>
|
||||
<p class="as-title-meta-sub">API 상태 모니터링 최근 반영 시각</p>
|
||||
<p class="as-title-meta-sub" th:if="${lastFireAt != null}"
|
||||
th:text="${#temporals.format(lastFireAt, 'yyyy-MM-dd HH:mm')} + ' KST · API 상태 모니터링 실행 시각'">-</p>
|
||||
<p class="as-title-meta-sub" th:if="${lastStatusChangedAt != null}"
|
||||
th:text="'마지막 상태 변경 ' + ${#temporals.format(lastStatusChangedAt, 'yyyy-MM-dd HH:mm')} + ' KST'">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,8 +83,9 @@
|
||||
<div class="as-filter-field">
|
||||
<label class="as-filter-label" for="filterKindSelect">유형</label>
|
||||
<select id="filterKindSelect" class="as-input">
|
||||
<option value="">장애 + 점검</option>
|
||||
<option value="">전체 유형</option>
|
||||
<option value="INCIDENT">장애만</option>
|
||||
<option value="DELAY">지연만</option>
|
||||
<option value="MAINTENANCE">점검만</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user