diff --git a/backend/openapi.json b/backend/openapi.json index b89417df..31ba2cc6 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -3966,10 +3966,6 @@ }, "status" : { "type" : "string" - }, - "details" : { - "type" : "object", - "additionalProperties" : { } } } }, diff --git a/backend/src/main/java/com/stackup/stackup/system/application/SystemHealthService.java b/backend/src/main/java/com/stackup/stackup/system/application/SystemHealthService.java index d2ef7a9c..ab2bc8f0 100644 --- a/backend/src/main/java/com/stackup/stackup/system/application/SystemHealthService.java +++ b/backend/src/main/java/com/stackup/stackup/system/application/SystemHealthService.java @@ -57,11 +57,9 @@ private SystemHealthResponse buildResponse(List specs) { private ComponentHealthResponse resolveComponent(ComponentSpec spec) { HealthDescriptor descriptor = resolveDescriptor(spec.actuatorPath()); if (descriptor == null) { - return new ComponentHealthResponse(spec.name(), Status.UNKNOWN.getCode(), Map.of()); + return new ComponentHealthResponse(spec.name(), Status.UNKNOWN.getCode()); } - - Map details = extractDetails(descriptor); - return new ComponentHealthResponse(spec.name(), descriptor.getStatus().getCode(), details); + return new ComponentHealthResponse(spec.name(), descriptor.getStatus().getCode()); } protected HealthDescriptor resolveDescriptor(String path) { @@ -72,13 +70,6 @@ protected HealthDescriptor resolveDescriptor(String path) { } } - private Map extractDetails(HealthDescriptor descriptor) { - try { - return Map.copyOf((Map) descriptor.getClass().getMethod("getDetails").invoke(descriptor)); - } catch (ReflectiveOperationException | ClassCastException ex) { - return Map.of(); - } - } private String aggregateStatus(Iterable components) { boolean hasUnknown = false; diff --git a/backend/src/main/java/com/stackup/stackup/system/application/dto/ComponentHealthResponse.java b/backend/src/main/java/com/stackup/stackup/system/application/dto/ComponentHealthResponse.java index 5af952ab..96423b3e 100644 --- a/backend/src/main/java/com/stackup/stackup/system/application/dto/ComponentHealthResponse.java +++ b/backend/src/main/java/com/stackup/stackup/system/application/dto/ComponentHealthResponse.java @@ -1,10 +1,10 @@ package com.stackup.stackup.system.application.dto; -import java.util.Map; - +// 공개(permitAll) 엔드포인트의 응답이므로 컴포넌트 이름과 상태만 담는다. +// 상세(버전·버킷·큐·적체량)는 인증 없이 흘리면 안 되고, 필요하면 호스트에서 +// Spring 자체 /actuator/health 를 본다(nginx 가 외부로 라우팅하지 않는다). public record ComponentHealthResponse( String name, - String status, - Map details + String status ) { } diff --git a/backend/src/test/java/com/stackup/stackup/system/application/SystemHealthServiceTest.java b/backend/src/test/java/com/stackup/stackup/system/application/SystemHealthServiceTest.java index 2a52e7ee..5ccba9ca 100644 --- a/backend/src/test/java/com/stackup/stackup/system/application/SystemHealthServiceTest.java +++ b/backend/src/test/java/com/stackup/stackup/system/application/SystemHealthServiceTest.java @@ -15,6 +15,7 @@ import org.springframework.boot.health.actuate.endpoint.StatusAggregator; import org.springframework.boot.health.contributor.Health; import org.springframework.boot.health.contributor.HealthIndicator; +import com.stackup.stackup.system.application.dto.ComponentHealthResponse; import org.springframework.boot.health.contributor.Status; import org.springframework.boot.health.registry.DefaultHealthContributorRegistry; import org.springframework.boot.health.registry.DefaultReactiveHealthContributorRegistry; @@ -47,7 +48,6 @@ void ready_usesDatabaseAndRabbitmqIndicators() { assertThat(response.status()).isEqualTo(Status.UP.getCode()); assertThat(response.components()).containsKeys("database", "rabbitmq"); assertThat(response.components()).doesNotContainKeys("s3", "aiServer"); - assertThat(response.components().get("database").details()).containsEntry("connections", 12); } @Test @@ -85,7 +85,6 @@ void health_readsRabbitFromActuatorKeyNotResponseKey() { // 응답 키는 그대로 rabbitmq — 공개 계약은 바뀌지 않는다. assertThat(response.components().get("rabbitmq").status()).isEqualTo(Status.UP.getCode()); - assertThat(response.components().get("rabbitmq").details()).containsEntry("version", "3.13"); assertThat(response.status()).isEqualTo(Status.UP.getCode()); } @@ -104,6 +103,30 @@ void health_reportsUnknownWhenActuatorHasNoSuchComponent() { assertThat(response.status()).isEqualTo(Status.UNKNOWN.getCode()); } + /** + * 공개(permitAll) 엔드포인트라 상세를 담지 않는다. + * + *

Actuator 는 기본값이 {@code show-details: never} 인데 이 서비스가 descriptor 에서 + * 상세를 직접 꺼내 쓰면서 그 보호를 우회하고 있었다 — RabbitMQ 버전·S3 버킷명·큐 이름과 + * 적체량이 인증 없이 나갔다. 상세가 필요하면 호스트에서 /actuator/health 를 본다. + */ + @Test + void health_doesNotExposeComponentDetails() { + HealthEndpoint healthEndpoint = healthEndpoint(Map.of( + "db", indicator(Status.UP, Map.of("database", "PostgreSQL")), + "rabbit", indicator(Status.UP, Map.of("version", "4.3.5")) + )); + SystemHealthService systemHealthService = new SystemHealthService(healthEndpoint); + + var response = systemHealthService.health(); + + // 상태는 그대로 전달되지만 상세는 응답 타입에 아예 없다. + assertThat(response.components().get("rabbitmq").status()).isEqualTo(Status.UP.getCode()); + assertThat(ComponentHealthResponse.class.getRecordComponents()) + .extracting(java.lang.reflect.RecordComponent::getName) + .containsExactly("name", "status"); + } + private static HealthEndpoint healthEndpoint(Map indicators) { HealthContributorRegistry registry = new DefaultHealthContributorRegistry(); indicators.forEach(registry::registerContributor); diff --git a/docs/observability.md b/docs/observability.md index a8ce9935..faf2278e 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -158,14 +158,20 @@ GET /api/system/health { "status": "UP", "components": { - "database": { "status": "UP" }, - "rabbitmq": { "status": "UP", "details": { "queues": 6 } }, - "s3": { "status": "UP" }, - "aiServer": { "status": "UP", "details": { "endpoint": "..." } } + "database": { "name": "database", "status": "UP" }, + "rabbitmq": { "name": "rabbitmq", "status": "UP" }, + "s3": { "name": "s3", "status": "UP" }, + "aiServer": { "name": "aiServer", "status": "UP" } } } ``` +> **상태만 담는다.** 이 엔드포인트는 permitAll 이라 인증 없이 열린다. Actuator 는 기본값이 +> `show-details: never` 인데 예전엔 이 서비스가 descriptor 에서 상세를 직접 꺼내 그 보호를 +> 우회했다 — RabbitMQ 버전·S3 버킷명·큐 이름과 적체량이 그대로 나갔다. +> 상세가 필요하면 **호스트에서** Spring 자체 `/actuator/health` 를 본다(nginx 가 외부로 +> 라우팅하지 않는다). + - Spring Boot Actuator 의 컴포넌트를 이름으로 조회해 재구성한다(`SystemHealthService`). - **응답 키와 Actuator 컴포넌트 키는 다르다.** Actuator 키는 Spring 이 등록하는 빈 이름에서 접미사를 뗀 값이라 `database`→`db`, `rabbitmq`→**`rabbit`** 이다. 여기를 틀리면 조회가 diff --git a/frontend/.env.example b/frontend/.env.example index 0c0306ac..eb23fe02 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -2,10 +2,10 @@ # 백엔드 Core API base URL (Authorization Bearer 호출 + /api/auth/*) # 임시라 상우가 바꾸면 바뀔 수 있습니다. -VITE_API_BASE_URL=https://www.udangtang.site +VITE_API_BASE_URL=https://stack-up.shop # SSE base URL (레거시 — RealTime 전환 후 미사용 예정) -VITE_SSE_BASE_URL=https://www.udangtang.site +VITE_SSE_BASE_URL=https://stack-up.shop # RealTime 서버 base URL (SSE /realtime/stream/* + WS /realtime/sessions/{id}) VITE_REALTIME_BASE_URL=http://localhost:38020 diff --git a/frontend/src/shared/api/generated.ts b/frontend/src/shared/api/generated.ts index b3aca3be..7d07171c 100644 --- a/frontend/src/shared/api/generated.ts +++ b/frontend/src/shared/api/generated.ts @@ -1376,9 +1376,6 @@ export interface components { ComponentHealthResponse: { name?: string; status?: string; - details?: { - [key: string]: unknown; - }; }; SystemHealthResponse: { status?: string;