Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions backend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3966,10 +3966,6 @@
},
"status" : {
"type" : "string"
},
"details" : {
"type" : "object",
"additionalProperties" : { }
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@ private SystemHealthResponse buildResponse(List<ComponentSpec> 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<String, Object> 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) {
Expand All @@ -72,13 +70,6 @@ protected HealthDescriptor resolveDescriptor(String path) {
}
}

private Map<String, Object> extractDetails(HealthDescriptor descriptor) {
try {
return Map.copyOf((Map<String, Object>) descriptor.getClass().getMethod("getDetails").invoke(descriptor));
} catch (ReflectiveOperationException | ClassCastException ex) {
return Map.of();
}
}

private String aggregateStatus(Iterable<ComponentHealthResponse> components) {
boolean hasUnknown = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> details
String status
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
}

Expand All @@ -104,6 +103,30 @@ void health_reportsUnknownWhenActuatorHasNoSuchComponent() {
assertThat(response.status()).isEqualTo(Status.UNKNOWN.getCode());
}

/**
* 공개(permitAll) 엔드포인트라 상세를 담지 않는다.
*
* <p>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<String, HealthIndicator> indicators) {
HealthContributorRegistry registry = new DefaultHealthContributorRegistry();
indicators.forEach(registry::registerContributor);
Expand Down
14 changes: 10 additions & 4 deletions docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`** 이다. 여기를 틀리면 조회가
Expand Down
4 changes: 2 additions & 2 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 0 additions & 3 deletions frontend/src/shared/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1376,9 +1376,6 @@ export interface components {
ComponentHealthResponse: {
name?: string;
status?: string;
details?: {
[key: string]: unknown;
};
};
SystemHealthResponse: {
status?: string;
Expand Down
Loading