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
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
@Service
public class SystemHealthService {

// 응답 키(name)와 Actuator 컴포넌트 키(actuatorPath)는 다르다.
// Actuator 키는 Spring 이 등록하는 빈 이름에서 접미사를 뗀 값이다 —
// DataSourceHealthContributor→"db", rabbitHealthContributor→"rabbit".
// "rabbitmq" 로 조회하면 항상 null 이라 rabbitmq 컴포넌트가 영구 UNKNOWN 이 된다.
private static final ComponentSpec DATABASE = new ComponentSpec("database", "db");
private static final ComponentSpec RABBITMQ = new ComponentSpec("rabbitmq", "rabbitmq");
private static final ComponentSpec RABBITMQ = new ComponentSpec("rabbitmq", "rabbit");
private static final ComponentSpec S3 = new ComponentSpec("s3", "s3");
private static final ComponentSpec AI_SERVER = new ComponentSpec("aiServer", "aiServer");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ void live_returnsUpStatus() {
void ready_usesDatabaseAndRabbitmqIndicators() {
HealthEndpoint healthEndpoint = healthEndpoint(Map.of(
"db", indicator(Status.UP, Map.of("connections", 12)),
"rabbitmq", indicator(Status.UP, Map.of("host", "localhost"))
"rabbit", indicator(Status.UP, Map.of("host", "localhost")) // Actuator 실제 키
));
SystemHealthService systemHealthService = new SystemHealthService(healthEndpoint);

Expand All @@ -54,7 +54,7 @@ void ready_usesDatabaseAndRabbitmqIndicators() {
void health_includesDatabaseRabbitmqS3AndAiServer() {
HealthEndpoint healthEndpoint = healthEndpoint(Map.of(
"db", indicator(Status.UP, Map.of()),
"rabbitmq", indicator(Status.DOWN, Map.of("reachable", false))
"rabbit", indicator(Status.DOWN, Map.of("reachable", false)) // Actuator 실제 키
));
SystemHealthService systemHealthService = new SystemHealthService(healthEndpoint);

Expand All @@ -66,6 +66,44 @@ void health_includesDatabaseRabbitmqS3AndAiServer() {
assertThat(response.components().get("aiServer").status()).isEqualTo(Status.UNKNOWN.getCode());
}

/**
* 응답 키와 Actuator 컴포넌트 키가 다르다는 사실 자체를 못 박는다.
*
* <p>운영에서 rabbitmq 가 영구 UNKNOWN 이었던 원인이 여기였다 — Actuator 는
* {@code rabbitHealthContributor} 빈을 "rabbit" 으로 등록하는데 "rabbitmq" 로 조회했다.
* 기존 테스트는 픽스처를 "rabbitmq" 로 등록해서 같은 실수를 그대로 재현하고 있었다.
*/
@Test
void health_readsRabbitFromActuatorKeyNotResponseKey() {
HealthEndpoint healthEndpoint = healthEndpoint(Map.of(
"db", indicator(Status.UP, Map.of()),
"rabbit", indicator(Status.UP, Map.of("version", "3.13"))
));
SystemHealthService systemHealthService = new SystemHealthService(healthEndpoint);

var response = systemHealthService.ready();

// 응답 키는 그대로 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());
}

// Actuator 에 없는 이름으로 조회하면 UNKNOWN 이 된다 — 회귀 시 이 테스트가 아니라
// 위 테스트가 깨지도록, 여기서는 '없을 때의 동작'만 확인한다.
@Test
void health_reportsUnknownWhenActuatorHasNoSuchComponent() {
HealthEndpoint healthEndpoint = healthEndpoint(Map.of(
"db", indicator(Status.UP, Map.of())
));
SystemHealthService systemHealthService = new SystemHealthService(healthEndpoint);

var response = systemHealthService.ready();

assertThat(response.components().get("rabbitmq").status()).isEqualTo(Status.UNKNOWN.getCode());
assertThat(response.status()).isEqualTo(Status.UNKNOWN.getCode());
}

private static HealthEndpoint healthEndpoint(Map<String, HealthIndicator> indicators) {
HealthContributorRegistry registry = new DefaultHealthContributorRegistry();
indicators.forEach(registry::registerContributor);
Expand Down
9 changes: 8 additions & 1 deletion docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,16 @@ GET /api/system/health
}
```

- Spring Boot Actuator + 커스텀 indicator
- Spring Boot Actuator 의 컴포넌트를 이름으로 조회해 재구성한다(`SystemHealthService`).
- **응답 키와 Actuator 컴포넌트 키는 다르다.** Actuator 키는 Spring 이 등록하는 빈 이름에서
접미사를 뗀 값이라 `database`→`db`, `rabbitmq`→**`rabbit`** 이다. 여기를 틀리면 조회가
null 을 돌려줘 그 컴포넌트가 **영구 UNKNOWN** 이 된다(에러가 아니라 조용한 무응답).
- `s3`·`aiServer` 는 아직 커스텀 indicator 가 없어 UNKNOWN 이다 → `/health` 의 종합 status 도
UNKNOWN 으로 고정된다. 종합 판단이 필요하면 현재는 `/ready`(database·rabbitmq)를 쓴다.
- K8s liveness: 단순 200 응답 (`/api/system/live`)
- K8s readiness: 의존성 포함 (`/api/system/ready`)
- 컨테이너 healthcheck 는 Spring 자체 `/actuator/health` 를 쓴다(docker-compose) — 이쪽은
Actuator 종합이라 RabbitMQ 장애를 정상적으로 잡는다.

---

Expand Down
Loading