diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java index bac9dd9..a897496 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java @@ -49,6 +49,12 @@ import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow; * 검수·게시해야 개발자포탈에 노출되며, {@code PUBLISH}면 탐지 즉시 노출된다. * {@code OFF} 여도 이미 열려있는 장애의 복구 처리(ERROR_END/DELAY_END)는 계속 동작한다.

* + *

개발자포탈 미게시 인터페이스 포함 여부는 {@code djb.apistatus.auto-incident-include-unpublished} + * (기본 {@code false}) 로 정한다. 기본값이면 게시 API 만 이슈에 담고, + * 게시 API 가 하나도 없는 탐지는 기록하지 않는다. {@code true}면 미게시 인터페이스도 + * 포함해 건수로 집계된다. 복구 처리는 이 설정과 무관하게 + * 이미 이슈에 담긴 API 를 대상으로 동작한다.

+ * *

점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다 * ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정). * 따라서 CONTROL 이벤트를 다시 기록하면 순환이 되므로 기록하지 않는다.

@@ -96,6 +102,12 @@ public class ApiStatusDetectionService { /** 오탐이 그대로 고객에게 노출되지 않도록 기본은 검수(DRAFT) */ private static final String DEFAULT_AUTO_INCIDENT_MODE = MODE_DRAFT; + /** 자동 탐지 이슈에 개발자포탈 미게시 인터페이스도 포함할지 여부 - true / false */ + public static final String KEY_INCLUDE_UNPUBLISHED = "djb.apistatus.auto-incident-include-unpublished"; + + /** 포탈 게시 API 장애만 고객 공지 대상이 되도록 기본은 제외(false) */ + private static final String DEFAULT_INCLUDE_UNPUBLISHED = "false"; + @Autowired private DjbApistatusIncidentRepository incidentRepository; @@ -208,6 +220,19 @@ public class ApiStatusDetectionService { } boolean autoPublish = MODE_PUBLISH.equals(mode); + // 미게시 인터페이스 제외 설정(false)이면 게시 API 만 남기고, 하나도 없으면 기록하지 않는다 + if (!resolveIncludeUnpublished()) { + List published = filterPublished(apiIds); + if (published.isEmpty()) { + log.info("게시 API 없음 - 미게시 인터페이스 제외 설정으로 기록하지 않음: {} {}", event, apiIds); + return; + } + if (published.size() < apiIds.size()) { + log.info("미게시 인터페이스 제외: {} → {}", apiIds, published); + } + apiIds = published; + } + LocalDateTime now = LocalDateTime.now(); String interfaceId = makeInterfaceId(event, apiIds, now); @@ -314,6 +339,27 @@ public class ApiStatusDetectionService { } } + /** + * 자동 탐지 이슈에 개발자포탈 미게시 인터페이스도 포함할지 여부. + * + *

PTL_PROPERTY({@code Portal} / {@code djb.apistatus.auto-incident-include-unpublished}) 를 읽는다. + * 최초 접근 시 기본값으로 row 가 자동 생성된다. 기본 {@code false} - 게시 API 만 이슈에 담는다. + * 명시적으로 {@code true}/{@code Y} 를 설정한 경우에만 미게시 인터페이스를 포함하며, + * 알 수 없는 값·조회 실패는 기본값 쪽(false)으로 떨어뜨린다.

+ */ + private boolean resolveIncludeUnpublished() { + try { + String value = portalPropertyService.getOrCreateProperty( + PROPERTY_GROUP, KEY_INCLUDE_UNPUBLISHED, DEFAULT_INCLUDE_UNPUBLISHED, + "API 상태 자동 탐지 이슈에 포탈 미게시 인터페이스 포함 여부(true/false)"); + String normalized = StringUtils.trimToEmpty(value); + return "true".equalsIgnoreCase(normalized) || "Y".equalsIgnoreCase(normalized); + } catch (Exception e) { + log.warn("미게시 인터페이스 포함 여부 조회 실패 - 기본값 {} 적용", DEFAULT_INCLUDE_UNPUBLISHED, e); + return false; + } + } + private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event, IncidentKind kind, List apiIds, LocalDateTime now, boolean autoPublish) { Long incidentId = incident.getIncidentId(); @@ -540,22 +586,46 @@ public class ApiStatusDetectionService { } /** - * 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다. + * 게시 인터페이스 ID 집합 조회. 실패 처리는 호출부 정책에 맡긴다. * *

판정 조건 두 개가 서로 다른 스키마에 있어 조회를 나눈다 - API 그룹 편성은 AGWAPP, * 스펙 존재는 EMSAPP(PTL_API_SPEC_INFO). 한 쿼리로 조인하면 tenant=APIGW 커넥션이 * EMSAPP 를 못 봐서 ORA-00942 가 난다.

*/ + private Set lookupPublishedApiIds(List apiIds) { + List groupedIds = apiStatusRepository.findDisplayedGroupApiIds(apiIds); + return groupedIds.isEmpty() + ? Collections.emptySet() + : new HashSet<>(apiSpecInfoRepository.findApiIdsIn(groupedIds)); + } + + /** + * 게시 API 만 남긴다 (미게시 인터페이스 제외 설정용). 판정에 실패하면 전체를 유지한다 - + * 제외 판단이 안 되는 상황에서 진짜 장애 기록까지 버리는 쪽이 더 위험하고, + * 명칭 노출은 표기 단계({@link #splitPublished})가 따로 막는다. + */ + private List filterPublished(List apiIds) { + try { + Set publishedIds = lookupPublishedApiIds(apiIds); + return apiIds.stream() + .filter(publishedIds::contains) + .collect(Collectors.toList()); + } catch (Exception e) { + log.warn("게시 API 판정 실패 - 미게시 인터페이스 제외 없이 전체 포함", e); + return apiIds; + } + } + + /** + * 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다. + */ private ApiSplit splitPublished(List apiIds) { if (apiIds == null || apiIds.isEmpty()) { return new ApiSplit(Collections.emptyList(), 0); } Set publishedIds; try { - List groupedIds = apiStatusRepository.findDisplayedGroupApiIds(apiIds); - publishedIds = groupedIds.isEmpty() - ? Collections.emptySet() - : new HashSet<>(apiSpecInfoRepository.findApiIdsIn(groupedIds)); + publishedIds = lookupPublishedApiIds(apiIds); } catch (Exception e) { // 실패 시 노출하는 쪽으로 기울면 내부 인터페이스가 고객 화면에 새어 나간다 log.warn("게시 API 판정 실패 - 전부 GW 인터페이스로 묶어 표기", e);