장애·지연 구분 및 집계 개선:
- 지연 유형 로직 및 UI/스타일 업데이트 - JPQL 조회와 공개 조건 로직 전면 개정
This commit is contained in:
+39
-24
@@ -15,31 +15,52 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 개발자포탈 API Status 화면 전용 조회. 읽기 전용이며 공개 조건(공지 게시 + 초안 아님)을 항상 적용한다.
|
* 개발자포탈 API Status 화면 전용 조회. 읽기 전용이며 공개 조건을 항상 적용한다.
|
||||||
*
|
*
|
||||||
* <p>PTL_NOTICE 와의 조인은 두 가지 역할을 한다.
|
* <p>공개 조건({@link #VISIBLE})은 두 갈래다.
|
||||||
* (1) 관리자가 미게시(USE_YN='N')한 장애/점검을 숨긴다.
|
* <ul>
|
||||||
* (2) 공지가 삭제된 고아 장애 행을 자연히 제외한다 (물리 FK 없음 - ADR-F10).</p>
|
* <li>공지가 붙은 이슈(장애·점검) - 그 공지가 게시(USE_YN='Y')되어 있어야 한다.
|
||||||
|
* 공지가 삭제된 고아 행도 이 조건에서 자연히 빠진다 (물리 FK 없음 - ADR-F10).</li>
|
||||||
|
* <li>공지가 없는 이슈(지연) - 자동 탐지 전용이라 검수할 공지가 없다.
|
||||||
|
* 초안(DRAFT_YN='Y')만 아니면 노출한다.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>공지 조건을 EXISTS 로 쓰는 이유: 예전처럼 {@code FROM ... , PortalNotice n} 으로 조인하면
|
||||||
|
* NOTICE_ID 가 없는 지연 이슈가 행 자체에서 사라진다.</p>
|
||||||
*/
|
*/
|
||||||
public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatusIncident, Long> {
|
public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatusIncident, Long> {
|
||||||
|
|
||||||
String VISIBLE = " i.noticeId = n.id AND n.useYn = 'Y' AND i.draftYn = 'N' ";
|
String VISIBLE = " i.draftYn = 'N'"
|
||||||
|
+ " AND (i.noticeId IS NULL"
|
||||||
|
+ " OR EXISTS (SELECT 1 FROM PortalNotice n"
|
||||||
|
+ " WHERE n.id = i.noticeId AND n.useYn = 'Y')) ";
|
||||||
|
|
||||||
|
/** 종결 판정이 STATE 로 이뤄지는 종류 (장애·지연). JPQL 리터럴로 써야 해서 FQCN 을 쓴다 */
|
||||||
|
String KIND_INCIDENT = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.INCIDENT";
|
||||||
|
String KIND_DELAY = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.DELAY";
|
||||||
|
String KIND_MAINTENANCE = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.MAINTENANCE";
|
||||||
|
|
||||||
|
/** 종결 조건 - 장애·지연은 STATE 로, 점검은 종료 시각 경과로 판정한다 */
|
||||||
|
String CLOSED_CONDITION = " ((i.kind IN (" + KIND_INCIDENT + ", " + KIND_DELAY + ")"
|
||||||
|
+ " AND i.state IN :closedStates)"
|
||||||
|
+ " OR (i.kind = " + KIND_MAINTENANCE
|
||||||
|
+ " AND i.endAt IS NOT NULL AND i.endAt < :now)) ";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중 장애 (P3)
|
* 진행 중 이슈 (P3). 장애와 지연을 함께 본다.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT i FROM DjbApistatusIncident i, PortalNotice n"
|
@Query("SELECT i FROM DjbApistatusIncident i"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND i.kind = :kind"
|
+ " AND i.kind IN :kinds"
|
||||||
+ " AND i.state NOT IN :closedStates"
|
+ " AND i.state NOT IN :closedStates"
|
||||||
+ " ORDER BY i.startedAt DESC")
|
+ " ORDER BY i.startedAt DESC")
|
||||||
List<DjbApistatusIncident> findVisibleOpenIncidents(@Param("kind") IncidentKind kind,
|
List<DjbApistatusIncident> findVisibleOpenIncidents(@Param("kinds") Collection<IncidentKind> kinds,
|
||||||
@Param("closedStates") Collection<IncidentState> closedStates);
|
@Param("closedStates") Collection<IncidentState> closedStates);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 예정/진행 중 점검 (P5). 종료 시각이 없거나 아직 지나지 않은 점검.
|
* 예정/진행 중 점검 (P5). 종료 시각이 없거나 아직 지나지 않은 점검.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT i FROM DjbApistatusIncident i, PortalNotice n"
|
@Query("SELECT i FROM DjbApistatusIncident i"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND i.kind = :kind"
|
+ " AND i.kind = :kind"
|
||||||
+ " AND (i.endAt IS NULL OR i.endAt >= :now)"
|
+ " AND (i.endAt IS NULL OR i.endAt >= :now)"
|
||||||
@@ -48,21 +69,15 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
|||||||
@Param("now") LocalDateTime now);
|
@Param("now") LocalDateTime now);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 종결된 이슈 (P6). 장애는 종결 상태, 점검은 종료 시각 경과.
|
* 종결된 이슈 (P6). 장애·지연은 종결 상태, 점검은 종료 시각 경과.
|
||||||
*/
|
*/
|
||||||
@Query(value = "SELECT i FROM DjbApistatusIncident i, PortalNotice n"
|
@Query(value = "SELECT i FROM DjbApistatusIncident i"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND ((i.kind = com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.INCIDENT"
|
+ " AND " + CLOSED_CONDITION
|
||||||
+ " AND i.state IN :closedStates)"
|
|
||||||
+ " OR (i.kind = com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.MAINTENANCE"
|
|
||||||
+ " AND i.endAt IS NOT NULL AND i.endAt < :now))"
|
|
||||||
+ " ORDER BY i.startedAt DESC",
|
+ " ORDER BY i.startedAt DESC",
|
||||||
countQuery = "SELECT COUNT(i) FROM DjbApistatusIncident i, PortalNotice n"
|
countQuery = "SELECT COUNT(i) FROM DjbApistatusIncident i"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND ((i.kind = com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.INCIDENT"
|
+ " AND " + CLOSED_CONDITION)
|
||||||
+ " AND i.state IN :closedStates)"
|
|
||||||
+ " OR (i.kind = com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.MAINTENANCE"
|
|
||||||
+ " AND i.endAt IS NOT NULL AND i.endAt < :now))")
|
|
||||||
Page<DjbApistatusIncident> findVisibleClosedIssues(@Param("closedStates") Collection<IncidentState> closedStates,
|
Page<DjbApistatusIncident> findVisibleClosedIssues(@Param("closedStates") Collection<IncidentState> closedStates,
|
||||||
@Param("now") LocalDateTime now,
|
@Param("now") LocalDateTime now,
|
||||||
Pageable pageable);
|
Pageable pageable);
|
||||||
@@ -71,7 +86,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
|||||||
* 기간과 겹치는 모든 이슈 (P2 가동률 집계, P9 일자 인덱스, P10 목록).
|
* 기간과 겹치는 모든 이슈 (P2 가동률 집계, P9 일자 인덱스, P10 목록).
|
||||||
* 진행 중(END_AT IS NULL) 이슈도 포함한다.
|
* 진행 중(END_AT IS NULL) 이슈도 포함한다.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT i FROM DjbApistatusIncident i, PortalNotice n"
|
@Query("SELECT i FROM DjbApistatusIncident i"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND i.startedAt < :to"
|
+ " AND i.startedAt < :to"
|
||||||
+ " AND (i.endAt IS NULL OR i.endAt >= :from)"
|
+ " AND (i.endAt IS NULL OR i.endAt >= :from)"
|
||||||
@@ -82,7 +97,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
|||||||
/**
|
/**
|
||||||
* 기간과 겹치고 특정 API 에 영향을 준 이슈 (P9/P10 의 apiId 필터).
|
* 기간과 겹치고 특정 API 에 영향을 준 이슈 (P9/P10 의 apiId 필터).
|
||||||
*/
|
*/
|
||||||
@Query("SELECT DISTINCT i FROM DjbApistatusIncident i, PortalNotice n, DjbApistatusIncidentApi a"
|
@Query("SELECT DISTINCT i FROM DjbApistatusIncident i, DjbApistatusIncidentApi a"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND a.incidentId = i.incidentId"
|
+ " AND a.incidentId = i.incidentId"
|
||||||
+ " AND a.apiId = :apiId"
|
+ " AND a.apiId = :apiId"
|
||||||
@@ -96,7 +111,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
|||||||
/**
|
/**
|
||||||
* 공개 상세 (P7)
|
* 공개 상세 (P7)
|
||||||
*/
|
*/
|
||||||
@Query("SELECT i FROM DjbApistatusIncident i, PortalNotice n"
|
@Query("SELECT i FROM DjbApistatusIncident i"
|
||||||
+ " WHERE " + VISIBLE
|
+ " WHERE " + VISIBLE
|
||||||
+ " AND i.incidentId = :incidentId")
|
+ " AND i.incidentId = :incidentId")
|
||||||
Optional<DjbApistatusIncident> findVisibleById(@Param("incidentId") Long incidentId);
|
Optional<DjbApistatusIncident> findVisibleById(@Param("incidentId") Long incidentId);
|
||||||
|
|||||||
+12
-11
@@ -149,10 +149,10 @@ public class ApiCurrentStatusService {
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 진행 중(미종결) 장애 중 API 별로 가장 심각한 한 건 */
|
/** 진행 중(미종결) 장애·지연 중 API 별로 가장 심각한 한 건 */
|
||||||
private Map<String, DjbApistatusIncident> mapOpenIncidents(Set<String> apiIds) {
|
private Map<String, DjbApistatusIncident> mapOpenIncidents(Set<String> apiIds) {
|
||||||
List<DjbApistatusIncident> openIncidents = incidentQueryRepository
|
List<DjbApistatusIncident> openIncidents = incidentQueryRepository
|
||||||
.findVisibleOpenIncidents(IncidentKind.INCIDENT, ApiStatusSupport.CLOSED_STATES);
|
.findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
|
||||||
if (openIncidents.isEmpty()) {
|
if (openIncidents.isEmpty()) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
@@ -204,12 +204,12 @@ public class ApiCurrentStatusService {
|
|||||||
return byApi;
|
return byApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 조회 기간 내 마지막 장애 발생 시각 */
|
/** 조회 기간 내 마지막 장애·지연 발생 시각 */
|
||||||
private Map<String, LocalDateTime> collectLastIncidentAt(Set<String> apiIds, LocalDateTime now, int windowDays) {
|
private Map<String, LocalDateTime> collectLastIncidentAt(Set<String> apiIds, LocalDateTime now, int windowDays) {
|
||||||
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
|
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
|
||||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
||||||
.filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
|
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (incidents.isEmpty()) {
|
if (incidents.isEmpty()) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
@@ -233,14 +233,15 @@ public class ApiCurrentStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중 장애 + 점검 여부로 현재 상태를 정한다.
|
* 진행 중 장애·지연 + 점검 여부로 현재 상태를 정한다.
|
||||||
*
|
*
|
||||||
* <p>자동 탐지 지연(INTERFACE_ID {@code DELAY_START:}) 과 모니터링 단계는 장애가 아닌 지연으로 본다.
|
* <p>KIND 가 DELAY 면 지연이다. 장애(INCIDENT)라도 모니터링 단계면 이미 완화된 상태라
|
||||||
* 이슈 이력 화면의 "지연/장애" 필터와 같은 기준이다.</p>
|
* 지연으로 표기한다 - 이슈 이력의 유형 필터(KIND 기준)와는 이 지점만 다르다.</p>
|
||||||
*/
|
*/
|
||||||
private String resolveStatus(DjbApistatusIncident open, boolean underMaintenance) {
|
private String resolveStatus(DjbApistatusIncident open, boolean underMaintenance) {
|
||||||
if (open != null) {
|
if (open != null) {
|
||||||
boolean degraded = open.getState() == IncidentState.MONITORING || ApiStatusSupport.isDelay(open);
|
boolean degraded = open.getKind() == IncidentKind.DELAY
|
||||||
|
|| open.getState() == IncidentState.MONITORING;
|
||||||
return degraded ? ApiStatusSupport.STATUS_DEGRADED : ApiStatusSupport.STATUS_OUTAGE;
|
return degraded ? ApiStatusSupport.STATUS_DEGRADED : ApiStatusSupport.STATUS_OUTAGE;
|
||||||
}
|
}
|
||||||
if (underMaintenance) {
|
if (underMaintenance) {
|
||||||
@@ -250,14 +251,14 @@ public class ApiCurrentStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 같은 API 에 열린 장애가 여럿일 때의 우선순위 (낮을수록 심각).
|
* 같은 API 에 열린 이슈가 여럿일 때의 우선순위 (낮을수록 심각).
|
||||||
* 지연으로 판정되는 건은 장애보다 뒤로 민다.
|
* 지연은 장애보다 뒤로 민다.
|
||||||
*/
|
*/
|
||||||
private int severity(DjbApistatusIncident incident) {
|
private int severity(DjbApistatusIncident incident) {
|
||||||
if (incident == null) {
|
if (incident == null) {
|
||||||
return 99;
|
return 99;
|
||||||
}
|
}
|
||||||
int base = ApiStatusSupport.isDelay(incident) ? 10 : 0;
|
int base = incident.getKind() == IncidentKind.DELAY ? 10 : 0;
|
||||||
IncidentState state = incident.getState();
|
IncidentState state = incident.getState();
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return base + 9;
|
return base + 9;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ public class ApiStatusAssembler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지난 이슈 카드. 장애는 타임라인 전체를 붙이고 점검은 붙이지 않는다 (ADR-F15).
|
* 지난 이슈 카드. 장애·지연은 타임라인 전체를 붙이고 점검은 붙이지 않는다 (ADR-F15).
|
||||||
*/
|
*/
|
||||||
public List<PastIssueCardDTO> toPastIssueCards(List<DjbApistatusIncident> incidents) {
|
public List<PastIssueCardDTO> toPastIssueCards(List<DjbApistatusIncident> incidents) {
|
||||||
if (incidents.isEmpty()) {
|
if (incidents.isEmpty()) {
|
||||||
@@ -135,7 +135,7 @@ public class ApiStatusAssembler {
|
|||||||
Map<Long, List<AffectedApiDTO>> apis = loadApis(ids);
|
Map<Long, List<AffectedApiDTO>> apis = loadApis(ids);
|
||||||
|
|
||||||
List<Long> incidentKindIds = incidents.stream()
|
List<Long> incidentKindIds = incidents.stream()
|
||||||
.filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
|
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
|
||||||
.map(DjbApistatusIncident::getIncidentId)
|
.map(DjbApistatusIncident::getIncidentId)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Map<Long, List<TimelineEntryDTO>> timelines = loadTimelines(incidentKindIds);
|
Map<Long, List<TimelineEntryDTO>> timelines = loadTimelines(incidentKindIds);
|
||||||
|
|||||||
+12
-27
@@ -30,9 +30,6 @@ import java.util.stream.Collectors;
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public class ApiStatusIssueHistoryService {
|
public class ApiStatusIssueHistoryService {
|
||||||
|
|
||||||
/** 지연은 IncidentKind 가 아니라 INCIDENT 의 하위 구분이므로 필터 값만 따로 둔다 */
|
|
||||||
private static final String KIND_DELAY = "DELAY";
|
|
||||||
|
|
||||||
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
||||||
private final ApiStatusAssembler assembler;
|
private final ApiStatusAssembler assembler;
|
||||||
private final ApiStatusCatalogService catalogService;
|
private final ApiStatusCatalogService catalogService;
|
||||||
@@ -85,19 +82,17 @@ public class ApiStatusIssueHistoryService {
|
|||||||
* 인덱스바 1칸에 이슈 1건을 집계한다. 유형은 이슈 목록 필터와 같은 세 축(장애/지연/점검)으로 가른다.
|
* 인덱스바 1칸에 이슈 1건을 집계한다. 유형은 이슈 목록 필터와 같은 세 축(장애/지연/점검)으로 가른다.
|
||||||
*/
|
*/
|
||||||
private void tally(IssueDateEntryDTO entry, DjbApistatusIncident incident) {
|
private void tally(IssueDateEntryDTO entry, DjbApistatusIncident incident) {
|
||||||
String kind;
|
IncidentKind kind = incident.getKind();
|
||||||
if (incident.getKind() == IncidentKind.MAINTENANCE) {
|
if (kind == null) {
|
||||||
entry.setMntCount(entry.getMntCount() + 1);
|
return;
|
||||||
kind = IncidentKind.MAINTENANCE.name();
|
|
||||||
} else if (ApiStatusSupport.isDelay(incident)) {
|
|
||||||
entry.setDlyCount(entry.getDlyCount() + 1);
|
|
||||||
kind = KIND_DELAY;
|
|
||||||
} else {
|
|
||||||
entry.setIncCount(entry.getIncCount() + 1);
|
|
||||||
kind = IncidentKind.INCIDENT.name();
|
|
||||||
}
|
}
|
||||||
if (!entry.getKinds().contains(kind)) {
|
switch (kind) {
|
||||||
entry.getKinds().add(kind);
|
case MAINTENANCE: entry.setMntCount(entry.getMntCount() + 1); break;
|
||||||
|
case DELAY: entry.setDlyCount(entry.getDlyCount() + 1); break;
|
||||||
|
default: entry.setIncCount(entry.getIncCount() + 1); break;
|
||||||
|
}
|
||||||
|
if (!entry.getKinds().contains(kind.name())) {
|
||||||
|
entry.getKinds().add(kind.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,24 +143,14 @@ public class ApiStatusIssueHistoryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 유형 필터. 미지정/ALL/알 수 없는 값은 전체(null)로 본다.
|
* 유형 필터. 미지정/ALL/알 수 없는 값은 전체(null)로 본다.
|
||||||
*
|
* 필터 값은 {@link IncidentKind} 이름 그대로다 (INCIDENT / DELAY / MAINTENANCE).
|
||||||
* <p>지연(DELAY)은 별도 KIND 가 아니라 자동 탐지 시 INCIDENT 로 흡수된다(ADR-F12).
|
|
||||||
* 자동 탐지 지연 건은 INTERFACE_ID 가 {@code DELAY_START:} 로 시작하므로 이를 기준으로 가른다.
|
|
||||||
* "장애만" 은 지연을 제외한 INCIDENT 다.</p>
|
|
||||||
*/
|
*/
|
||||||
private java.util.function.Predicate<DjbApistatusIncident> kindFilter(String kind) {
|
private java.util.function.Predicate<DjbApistatusIncident> kindFilter(String kind) {
|
||||||
if (StringUtils.isBlank(kind) || "ALL".equalsIgnoreCase(kind)) {
|
if (StringUtils.isBlank(kind) || "ALL".equalsIgnoreCase(kind)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (KIND_DELAY.equalsIgnoreCase(kind)) {
|
|
||||||
return ApiStatusSupport::isDelay;
|
|
||||||
}
|
|
||||||
if ("INCIDENT".equalsIgnoreCase(kind)) {
|
|
||||||
return incident -> incident.getKind() == IncidentKind.INCIDENT
|
|
||||||
&& !ApiStatusSupport.isDelay(incident);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
IncidentKind selected = IncidentKind.valueOf(kind.toUpperCase());
|
IncidentKind selected = IncidentKind.valueOf(kind.trim().toUpperCase());
|
||||||
return incident -> incident.getKind() == selected;
|
return incident -> incident.getKind() == selected;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+2
-2
@@ -26,11 +26,11 @@ public class ApiStatusQueryService {
|
|||||||
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
||||||
private final ApiStatusAssembler assembler;
|
private final ApiStatusAssembler assembler;
|
||||||
|
|
||||||
/** P3 - 진행 중 장애 */
|
/** P3 - 진행 중 장애·지연 */
|
||||||
public List<ActiveIncidentDTO> getActiveIncidents() {
|
public List<ActiveIncidentDTO> getActiveIncidents() {
|
||||||
LocalDateTime now = ApiStatusSupport.now();
|
LocalDateTime now = ApiStatusSupport.now();
|
||||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||||
.findVisibleOpenIncidents(IncidentKind.INCIDENT, ApiStatusSupport.CLOSED_STATES);
|
.findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
|
||||||
return assembler.toActiveIncidents(incidents, now);
|
return assembler.toActiveIncidents(incidents, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ public final class ApiStatusSupport {
|
|||||||
public static final String STATUS_OUTAGE = "OUTAGE";
|
public static final String STATUS_OUTAGE = "OUTAGE";
|
||||||
public static final String STATUS_MAINTENANCE = "MAINTENANCE";
|
public static final String STATUS_MAINTENANCE = "MAINTENANCE";
|
||||||
|
|
||||||
/**
|
|
||||||
* 자동 탐지 지연 건의 INTERFACE_ID prefix (admin ApiStatusDetectionService 가 event 명으로 생성).
|
|
||||||
*
|
|
||||||
* <p>지연(DELAY)은 별도 KIND 가 아니라 INCIDENT 로 흡수되므로(ADR-F12) 이 prefix 로 가른다.</p>
|
|
||||||
*/
|
|
||||||
public static final String DELAY_INTERFACE_PREFIX = "DELAY_START:";
|
|
||||||
|
|
||||||
private ApiStatusSupport() {
|
private ApiStatusSupport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +38,22 @@ public final class ApiStatusSupport {
|
|||||||
return LocalDateTime.now(ZONE);
|
return LocalDateTime.now(ZONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 자동 탐지된 지연 건인지 여부 (장애와 구분) */
|
/** 지연 건인지 여부 (장애와 구분) */
|
||||||
public static boolean isDelay(DjbApistatusIncident incident) {
|
public static boolean isDelay(DjbApistatusIncident incident) {
|
||||||
return incident != null
|
return incident != null && incident.getKind() == IncidentKind.DELAY;
|
||||||
&& incident.getKind() == IncidentKind.INCIDENT
|
}
|
||||||
&& incident.getInterfaceId() != null
|
|
||||||
&& incident.getInterfaceId().startsWith(DELAY_INTERFACE_PREFIX);
|
/** 이슈 종류에 대응하는 현재 상태 코드 */
|
||||||
|
public static String statusOf(IncidentKind kind) {
|
||||||
|
if (kind == null) {
|
||||||
|
return STATUS_NORMAL;
|
||||||
|
}
|
||||||
|
switch (kind) {
|
||||||
|
case INCIDENT: return STATUS_OUTAGE;
|
||||||
|
case DELAY: return STATUS_DEGRADED;
|
||||||
|
case MAINTENANCE: return STATUS_MAINTENANCE;
|
||||||
|
default: return STATUS_NORMAL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-2
@@ -64,9 +64,10 @@ public class ApiStatusUptimeService {
|
|||||||
LocalDateTime now = ApiStatusSupport.now();
|
LocalDateTime now = ApiStatusSupport.now();
|
||||||
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
|
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
|
||||||
|
|
||||||
|
// 장애와 지연 모두 서비스 저하이므로 가동률에서 차감한다. 점검은 계획된 작업이라 제외.
|
||||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
||||||
.filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
|
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Map<String, List<long[]>> intervalsByApi = new HashMap<>();
|
Map<String, List<long[]>> intervalsByApi = new HashMap<>();
|
||||||
@@ -135,7 +136,7 @@ public class ApiStatusUptimeService {
|
|||||||
maintenanceIntervals.add(interval);
|
maintenanceIntervals.add(interval);
|
||||||
} else {
|
} else {
|
||||||
incidentIntervals.add(interval);
|
incidentIntervals.add(interval);
|
||||||
if (ApiStatusSupport.isDelay(incident)) {
|
if (incident.getKind() == IncidentKind.DELAY) {
|
||||||
delayIntervals.add(interval);
|
delayIntervals.add(interval);
|
||||||
} else {
|
} else {
|
||||||
outageIntervals.add(interval);
|
outageIntervals.add(interval);
|
||||||
|
|||||||
@@ -27221,6 +27221,9 @@ input[type=checkbox]:checked + .custom-checkbox {
|
|||||||
.api-status .as-issue-card.is-incident {
|
.api-status .as-issue-card.is-incident {
|
||||||
border-left: 4px solid var(--as-err);
|
border-left: 4px solid var(--as-err);
|
||||||
}
|
}
|
||||||
|
.api-status .as-issue-card.is-degraded {
|
||||||
|
border-left: 4px solid var(--as-warn);
|
||||||
|
}
|
||||||
.api-status .as-issue-card.is-maintenance {
|
.api-status .as-issue-card.is-maintenance {
|
||||||
border-left: 4px solid var(--as-info);
|
border-left: 4px solid var(--as-info);
|
||||||
}
|
}
|
||||||
@@ -27243,6 +27246,9 @@ input[type=checkbox]:checked + .custom-checkbox {
|
|||||||
.api-status .as-issue-card.is-incident .as-issue-title {
|
.api-status .as-issue-card.is-incident .as-issue-title {
|
||||||
color: var(--as-err);
|
color: var(--as-err);
|
||||||
}
|
}
|
||||||
|
.api-status .as-issue-card.is-degraded .as-issue-title {
|
||||||
|
color: var(--as-warn);
|
||||||
|
}
|
||||||
.api-status .as-issue-card.is-maintenance .as-issue-title {
|
.api-status .as-issue-card.is-maintenance .as-issue-title {
|
||||||
color: var(--as-info);
|
color: var(--as-info);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
|
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
|
||||||
const linkableApiIds = new Set();
|
const linkableApiIds = new Set();
|
||||||
|
|
||||||
const KIND_LABEL = { INCIDENT: '장애', MAINTENANCE: '점검' };
|
const KIND_LABEL = { INCIDENT: '장애', DELAY: '지연', MAINTENANCE: '점검' };
|
||||||
|
const KIND_CLASS = { INCIDENT: 'is-incident', DELAY: 'is-degraded', MAINTENANCE: 'is-maintenance' };
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
const API_LIST_LIMIT = 50; // 라이브서치 결과 표시 상한
|
const API_LIST_LIMIT = 50; // 라이브서치 결과 표시 상한
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function issueCardHtml(issue) {
|
function issueCardHtml(issue) {
|
||||||
const kindClass = issue.kind === 'MAINTENANCE' ? 'is-maintenance' : 'is-incident';
|
const kindClass = KIND_CLASS[issue.kind] || 'is-incident';
|
||||||
|
|
||||||
let inner;
|
let inner;
|
||||||
if (issue.kind === 'MAINTENANCE') {
|
if (issue.kind === 'MAINTENANCE') {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
|
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
|
||||||
const linkableApiIds = new Set();
|
const linkableApiIds = new Set();
|
||||||
|
|
||||||
const KIND_LABEL = { INCIDENT: '장애', MAINTENANCE: '점검' };
|
const KIND_LABEL = { INCIDENT: '장애', DELAY: '지연', MAINTENANCE: '점검' };
|
||||||
|
const KIND_CLASS = { INCIDENT: 'is-incident', DELAY: 'is-degraded', MAINTENANCE: 'is-maintenance' };
|
||||||
|
|
||||||
/** 진행 중 장애 카드 노출 상한. 초과분은 전체 이력 링크로 넘긴다. */
|
/** 진행 중 장애 카드 노출 상한. 초과분은 전체 이력 링크로 넘긴다. */
|
||||||
const ACTIVE_INCIDENT_LIMIT = 1;
|
const ACTIVE_INCIDENT_LIMIT = 1;
|
||||||
@@ -262,8 +263,8 @@
|
|||||||
|
|
||||||
// ---------------- ❺ 지난 이슈 사항 ----------------
|
// ---------------- ❺ 지난 이슈 사항 ----------------
|
||||||
function issueCardHtml(issue) {
|
function issueCardHtml(issue) {
|
||||||
const kindClass = issue.kind === 'MAINTENANCE' ? 'is-maintenance' : 'is-incident';
|
const kindClass = KIND_CLASS[issue.kind] || 'is-incident';
|
||||||
const kindBadge = issue.kind === 'MAINTENANCE' ? 'is-maintenance' : 'is-incident';
|
const kindBadge = kindClass;
|
||||||
|
|
||||||
let inner;
|
let inner;
|
||||||
if (issue.kind === 'MAINTENANCE') {
|
if (issue.kind === 'MAINTENANCE') {
|
||||||
|
|||||||
@@ -495,6 +495,7 @@
|
|||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||||
|
|
||||||
&.is-incident { border-left: 4px solid var(--as-err); }
|
&.is-incident { border-left: 4px solid var(--as-err); }
|
||||||
|
&.is-degraded { border-left: 4px solid var(--as-warn); }
|
||||||
&.is-maintenance { border-left: 4px solid var(--as-info); }
|
&.is-maintenance { border-left: 4px solid var(--as-info); }
|
||||||
|
|
||||||
.as-issue-meta-row {
|
.as-issue-meta-row {
|
||||||
@@ -516,6 +517,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.is-incident .as-issue-title { color: var(--as-err); }
|
&.is-incident .as-issue-title { color: var(--as-err); }
|
||||||
|
&.is-degraded .as-issue-title { color: var(--as-warn); }
|
||||||
&.is-maintenance .as-issue-title { color: var(--as-info); }
|
&.is-maintenance .as-issue-title { color: var(--as-info); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user