292 lines
20 KiB
Markdown
292 lines
20 KiB
Markdown
---
|
|
title: branch / feature-startup-failure-log-suppression
|
|
source_type: branch-note
|
|
status: raw
|
|
branch: feature-startup-failure-log-suppression
|
|
parent_branch:
|
|
related_projects: [ca-tmpl]
|
|
tags: [branch, ca-tmpl, runtime, spring-boot, error-handling, flyway, log-routing]
|
|
created: 2026-07-03
|
|
target_merge:
|
|
status_label: in-progress
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-058
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-058
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-BOOTSTRAP-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-001, WI-CA-SKELETON-OPERATIONAL-CONTRACT-017]
|
|
contract_packet: 1
|
|
contract_packet_sha256: e7c1afc830ee67bc838ff152355faa14fe6c26669d8914655bda307ea186532b
|
|
---
|
|
|
|
# branch: feature-startup-failure-log-suppression
|
|
|
|
> Layer: `raw/branch-notes/` — ca-tmpl startup failure logging 작업 기록.
|
|
> 실제 git branch: `develop`.
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
- [[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: suppressible startup failure 조건과 retained actionable error test가 명시된다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-BOOTSTRAP-001@1` | 신규 환경의 default 진입 명령은 ./gradlew bootstrap이다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1` | foundation이 envelope schema와 error.category enum의 단일 owner다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
- Spring Boot startup failure에서 ca-tmpl의 구조화 `MIGRATION_FAILED` 로그와 Spring Boot 기본
|
|
`Application run failed` stacktrace가 함께 출력되는 문제를 줄인다.
|
|
- 목표 정책: startup failure는 `startup.phase`, `error.code`, `error.category`, root-cause 요약만
|
|
남기고 framework/driver stacktrace는 application log에 출력하지 않는다.
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- `app-bootstrap` startup failure logging 계약 변경.
|
|
- `StartupFailures` canonical log에서 SLF4J throwable 인자 제거.
|
|
- `SpringBootExceptionReporter`로 typed startup failure의 Spring Boot 기본 실패 report 억제.
|
|
- Logback `TurboFilter`로 startup failure 이후의 SpringApplication 중복 close/report message 억제.
|
|
- settings/validator startup failure를 `StartupFailures.envValidation(...)`로 통일.
|
|
- Spring Boot context refresh cancellation / failure analysis residual log 억제.
|
|
- focused tests, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies`, `check` 검증.
|
|
|
|
### 제외 범위
|
|
|
|
- runtime HTTP exception response shape 변경.
|
|
- persistence `DB_*` SQLState matrix 변경.
|
|
- production 환경 로그 검증.
|
|
|
|
## 근거
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| Local code evidence: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupFailures.java` | 기존 canonical startup failure log가 throwable cause를 SLF4J에 넘겨 stacktrace를 출력하던 사실 확인. |
|
|
| Local dependency evidence: `javap org.springframework.boot.SpringApplication` | `SpringBootExceptionReporter#reportException`이 `true`를 반환하면 Spring Boot가 failure를 logged exception으로 등록하고 generic report path를 종료하는 흐름 확인. |
|
|
| Local test evidence: `./gradlew check` | 전체 Gradle guard 통과로 구현·검증 결과 확인. |
|
|
|
|
## TODO
|
|
|
|
- [x] Startup failure canonical log에서 throwable proxy 제거 — 등급: `actually-implemented`
|
|
- [x] root-cause class/message 구조화 필드 추가 — 등급: `actually-implemented`
|
|
- [x] startup failure 전용 `SpringBootExceptionReporter` 등록 — 등급: `actually-implemented`
|
|
- [x] SpringApplication 중복 close/report log filter 추가 — 등급: `actually-implemented`
|
|
- [x] app-bootstrap settings/validator plain startup exception을 `StartupValidationException`으로 번역 — 등급: `actually-implemented`
|
|
- [x] Spring Boot context cancellation / failure analysis residual log filter 확장 — 등급: `actually-implemented`
|
|
- [x] DB down / invalid tracing sample rate `bootRun` 재현 검증 — 등급: `locally-verified`
|
|
- [x] focused tests, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies` 실행 — 등급: `locally-verified`
|
|
|
|
## 진행 중 메모
|
|
|
|
- TDD RED로 `StartupFailuresTest`가 기존 throwable proxy 때문에 실패하는 것을 먼저 확인했다.
|
|
- `SpringBootExceptionReporter`만으로는 `Unable to close ApplicationContext` WARN을 제어하지 못하므로,
|
|
canonical startup failure가 이미 기록된 뒤 SpringApplication의 exact duplicate message만 차단하는
|
|
Logback filter를 추가했다.
|
|
- `./gradlew check` 첫 실행은 Spotless formatting 위반으로 실패했고, `:app-bootstrap:spotlessApply`
|
|
적용 후 재실행에서 통과했다.
|
|
- 2026-07-03 후속 hardening: `ConfigurationProperties` record와 runtime startup validator가 plain
|
|
`IllegalStateException`/`IllegalArgumentException`을 던지던 사각을 `StartupFailures.envValidation(...)`
|
|
으로 통일했다.
|
|
- invalid tracing sample rate 재현에서 기존 `BindException`/`NumberFormatException`/FailureAnalysis 출력은
|
|
compact `STARTUP_VALIDATION_FAILED` 로그 1줄로 축소되었다.
|
|
- DB down migration 재현에서 기존 context refresh cancellation `BeanCreationException` WARN은 더 이상
|
|
grep 결과에 나타나지 않았다. 남은 Spring `BeanPostProcessorChecker`/Micrometer WARN은 exception report가
|
|
아니라 별도 framework lifecycle/noise 축이다.
|
|
|
|
## 결정 사항
|
|
|
|
- 2026-07-03: startup failure log는 stacktrace 대신 root-cause summary field만 남긴다 / 이유:
|
|
운영자가 분류할 수 있는 정보는 유지하면서 driver/framework stacktrace 노출과 중복을 줄이기 위해 /
|
|
검토한 대안: 중복 제거만, profile별 stacktrace 분기 / 근거: local code + test evidence.
|
|
- 2026-07-03: Spring Boot generic `Application run failed`는 `SpringBootExceptionReporter`로 typed
|
|
startup failure에 한해 억제한다 / 이유: unknown startup failure의 Boot 기본 진단은 유지하기 위해 /
|
|
검토한 대안: `org.springframework.boot.SpringApplication` logger 전체 off / 근거: local dependency
|
|
evidence.
|
|
- 2026-07-03: context close 중복 WARN은 marker 기반 Logback filter로 exact message만 차단한다 / 이유:
|
|
reporter 이후 context close 단계에서 발생하는 별도 SpringApplication WARN을 좁은 범위로 억제하기 위해 /
|
|
검토한 대안: logger 전체 off, 방치 / 근거: attached runtime log + local test evidence.
|
|
- 2026-07-03: startup validation/settings failure는 plain Java exception 대신 `StartupFailures.envValidation(...)`
|
|
으로 번역한다 / 이유: Boot binding/context failure path에 들어가더라도 cause chain에
|
|
`StartupFailureException`이 포함돼 compact reporter/filter 정책이 적용되게 하기 위해 / 검토한 대안:
|
|
reporter가 모든 `IllegalStateException`을 잡도록 확장, Spring Boot failure analyzer logger만 억제 / 근거:
|
|
local bootRun 재현 + focused tests.
|
|
- 2026-07-03: `LoggingFailureAnalysisReporter`와 Spring context refresh cancellation log는 startup failure
|
|
marker가 켜진 뒤에만 filter에서 억제한다 / 이유: unknown boot failure 진단은 보존하고, 이미 compact
|
|
startup failure가 기록된 중복 exception detail만 제거하기 위해 / 검토한 대안: Spring logger level 조정,
|
|
failure analysis reporter 전체 비활성 / 근거: local bootRun 재현 + filter tests.
|
|
|
|
## 결정-근거 매핑
|
|
|
|
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|
|
|
| D1 | `StartupFailures`는 cause를 예외에는 보존하되 SLF4J throwable 인자로 넘기지 않고 root-cause class/message만 로그 구조화 필드로 남긴다. | UNSUPPORTED_DECISION — local code inspection and user-approved policy; trade-off: stacktrace triage detail is removed from startup logs. | actually-implemented + locally-verified | 운영자가 전체 stacktrace를 로그에서 바로 보지 못하므로 재현 환경에서 cause chain 확인이 필요할 수 있다. |
|
|
| D2 | `StartupFailureExceptionReporter`는 cause chain에 `StartupFailureException`이 있을 때만 `true`를 반환해 Boot generic failure report를 억제한다. | UNSUPPORTED_DECISION — local `javap` inspection of Spring Boot failure reporting path; trade-off: official doc raw source was not captured in this task. | actually-implemented + locally-verified | Spring Boot internal flow가 major upgrade에서 바뀌면 reporter 효과를 재검증해야 한다. |
|
|
| D3 | `StartupFailureSpringBootLogFilter`는 canonical startup failure 이후 SpringApplication의 `Application run failed`와 `Unable to close ApplicationContext` exact message만 차단한다. | UNSUPPORTED_DECISION — attached log symptom + local filter tests; trade-off: process-local marker assumes startup failure is fatal. | actually-implemented + locally-verified | 동일 JVM에서 startup failure 후 테스트가 계속되는 특수 상황은 marker reset test helper에 의존한다. |
|
|
| D4 | Wiki branch-note slug는 logical work unit `feature-startup-failure-log-suppression`을 사용하고, 실제 git branch `develop`은 본문에 기록한다. | UNSUPPORTED_DECISION — LLM Wiki naming lint rejects `develop.md`; trade-off: ca-tmpl branch-name capture와 wiki naming gate 사이의 충돌을 wiki document shape 우선으로 해결. | documented-only | git branch 기준 검색 시 logical note slug를 한 번 더 확인해야 한다. |
|
|
| D5 | app-bootstrap startup settings/validators는 invalid config를 plain Java exception이 아니라 `StartupFailures.envValidation(...)`으로 던진다. | UNSUPPORTED_DECISION — local bug reproduction and user-approved policy; trade-off: direct constructor tests now observe `StartupValidationException` instead of `IllegalArgumentException`. | actually-implemented + locally-verified | `LoggingSettings`처럼 startup validation owner가 아닌 warn-and-default bootstrap helper는 별도 정책 예외로 남는다. |
|
|
| D6 | startup failure marker 이후 `LoggingFailureAnalysisReporter`와 Spring context refresh cancellation log를 filter에서 억제한다. | UNSUPPORTED_DECISION — local bootRun symptom and filter tests; trade-off: fatal startup failure window에서 Spring Boot failure-analysis banner를 숨긴다. | actually-implemented + locally-verified | Spring Boot logger/message 이름이 major upgrade에서 바뀌면 bootRun 재현 테스트로 재확인 필요. |
|
|
|
|
## 구현 가이드
|
|
|
|
### 1. Canonical startup log
|
|
|
|
> **Trace**: D1
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: structured field 이름은 `error.root_cause.class`와
|
|
> `error.root_cause.message`를 사용했다. 기존 `error.code`/`error.category` dotted naming과 맞춘
|
|
> local convention이다.
|
|
|
|
| File | 구현 |
|
|
|---|---|
|
|
| `StartupFailures.java` | cause가 있을 때 `rootCause(cause)`를 찾아 class/message를 structured argument로 기록하고, throwable 인자는 넘기지 않는다. |
|
|
| `StartupFailuresTest.java` | migration failure log event의 `ThrowableProxy`가 null이고 root-cause summary field가 있는지 검증한다. |
|
|
|
|
### 2. Spring Boot duplicate report suppression
|
|
|
|
> **Trace**: D2, D3
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `SpringBootExceptionReporter`와 Logback `TurboFilter`를 함께 사용했다.
|
|
> reporter는 generic report path만 막고, filter는 reporter 이후 context close duplicate WARN만 좁게 막는다.
|
|
|
|
| File | 구현 |
|
|
|---|---|
|
|
| `StartupFailureExceptionReporter.java` | cause chain에 `StartupFailureException`이 있으면 `true`, 아니면 `false`. |
|
|
| `META-INF/spring.factories` | `org.springframework.boot.SpringBootExceptionReporter` key로 reporter 등록. |
|
|
| `StartupFailureLogState.java` | canonical startup failure가 기록됐는지 process-local marker 제공. |
|
|
| `StartupFailureSpringBootLogFilter.java` | marker가 켜진 뒤 `org.springframework.boot.SpringApplication`의 exact duplicate messages만 `DENY`. |
|
|
| `logback-spring.xml` | startup failure duplicate filter를 turbo filter로 등록. |
|
|
|
|
### 3. Startup validation translation hardening
|
|
|
|
> **Trace**: D5, D6
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: settings record compact constructor도 `StartupFailures.envValidation(...)`
|
|
> 을 직접 호출한다. `app-bootstrap`의 운영 설정 검증이며 비즈니스 규칙이 아니므로 composition-root
|
|
> 책임 안에 둔다.
|
|
|
|
| File | 구현 |
|
|
|---|---|
|
|
| `AsyncExecutorSettings.java`, `IdempotencySettings.java`, `OutboxSettings.java`, `TracingSettings.java` | invalid runtime setting을 `StartupFailures.envValidation(...)`으로 변환한다. |
|
|
| `RuntimeNumericBoundsValidator.java`, `HikariPoolConstraintValidator.java`, `OpenInViewSafetyValidator.java`, `SecretSourceValidator.java` | startup safety guard의 plain exception을 `StartupFailures.envValidation(...)`으로 변환한다. |
|
|
| `StartupFailureSpringBootLogFilter.java` | marker 이후 `LoggingFailureAnalysisReporter` 전체와 Spring context refresh cancellation prefix를 `DENY`한다. |
|
|
| focused settings/validator/filter tests | invalid path가 `StartupValidationException`으로 번역되고 residual Boot logs가 filter에서 차단되는지 검증한다. |
|
|
|
|
## 엣지·실패·의존
|
|
|
|
- **실패·엣지 경로**: cause chain self-reference는 reporter와 root cause walker가 무한 루프를 피해야 한다.
|
|
- **실패·엣지 경로**: non-startup exception은 Spring Boot 기본 failure report를 유지해야 한다.
|
|
- **다른 계약 의존**: `app-bootstrap` logging bootstrap과 Spring Boot `spring.factories` loading path에 의존한다.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| 실제 bootRun에서 DB down 시 `Application run failed`, context refresh cancellation, `BeanCreationException` stacktrace가 출력되지 않는다. | 2026-07-03 후속 hardening에서 실제 재현 수행. | `APP_DATASOURCE_URL='jdbc:postgresql://127.0.0.1:1/ca_skeleton' timeout 60s ./gradlew :app-bootstrap:bootRun --quiet 2>&1 \| rg -n "startup failure\|Application run failed\|Exception encountered during context initialization\|LoggingFailureAnalysisReporter\|BeanCreationException\|ConnectException\|Caused by:" -C 2` | locally-verified |
|
|
| 실제 bootRun에서 invalid tracing sample rate가 `BindException`/`NumberFormatException` stacktrace 대신 compact startup failure로 출력된다. | 2026-07-03 후속 hardening에서 실제 재현 수행. | `APP_MIGRATION_ON_STARTUP=false APP_TRACING_SAMPLE_RATE='not-a-number' timeout 60s ./gradlew :app-bootstrap:bootRun --quiet 2>&1 \| rg -n "startup failure\|Application run failed\|Exception encountered during context initialization\|LoggingFailureAnalysisReporter\|BindException\|NumberFormatException\|Caused by:" -C 2` | locally-verified |
|
|
| Spring Boot major upgrade 후에도 `SpringBootExceptionReporter`의 true-return behavior가 동일하다. | local dependency bytecode 확인에 기반한 결정이다. | Spring Boot upgrade branch에서 reporter focused test와 실제 startup failure 로그 재현. | needs-confirmation |
|
|
|
|
## 관심사 커버리지
|
|
|
|
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
|
|--------|------|-------|--------|------|
|
|
| startup failure canonical log | covered-here | — | — | D1 |
|
|
| Spring Boot duplicate failure report | covered-here | — | — | D2, D3, D6 |
|
|
| app-bootstrap startup validation/settings plain exception | covered-here | — | — | D5 |
|
|
| runtime HTTP exception response | delegated | existing adapter-web error contract | OK | Out of scope |
|
|
| runtime background `log(..., ex)` stacktrace | delegated | future runtime logging hardening | OK | Out of scope |
|
|
|
|
## 마주친 문제
|
|
|
|
- Spotless formatting failure
|
|
- 원인: 새 Java 파일의 line wrapping이 Spotless 규칙과 달랐다.
|
|
- 시도: `./gradlew check` 실행.
|
|
- 해결: `./gradlew :app-bootstrap:spotlessApply` 후 `./gradlew check` 재실행.
|
|
- 별도 에러 노트로 분리됨: [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]]
|
|
- 2026-07-03 후속 hardening 중 TDD RED failures
|
|
- 원인: 의도적으로 settings/validator tests를 `StartupValidationException` 기대치로 먼저 바꿔 기존 plain exception 사각을 재현.
|
|
- 해결: `StartupFailures.envValidation(...)` 전환 후 focused suite green.
|
|
- 별도 에러 노트: 없음. 의도된 RED 단계로 별도 트러블슈팅 문서화 대상 아님.
|
|
- 2026-07-03 전체 `check` 실패
|
|
- 원인: `:sample-portfolio:test` web context startup 중 Tomcat `PortInUseException`/`BindException`.
|
|
- 시도: import order Spotless failure 수정 후 `./gradlew check` 재실행.
|
|
- 해결: 이번 변경 범위 밖의 sample-portfolio test/runtime port collision으로 분리 기록. focused startup/logging suite, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies`, bootRun 재현은 통과/확인.
|
|
- 별도 에러 노트로 분리됨: [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]]
|
|
|
|
## 묶음 (이 branch에서 파생된 자료)
|
|
|
|
<!-- GENERATED: errors:start -->
|
|
- [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]]
|
|
- [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]]
|
|
<!-- GENERATED: errors:end -->
|
|
|
|
### Sub-branches (세부 작업)
|
|
|
|
- 없음.
|
|
|
|
### 오류 기록 (이 branch 작업 중 발생)
|
|
|
|
- [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]] — `check` 중 Spotless formatting failure.
|
|
- [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]] — 전체 `check` 중 sample-portfolio Tomcat port collision.
|
|
- 2026-07-03 후속 hardening: TDD RED와 경로 오입력은 작업 중 검증/도구 사용 이슈로 branch-note에만 기록.
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- 추출할 별도 면접 질문 없음.
|
|
|
|
### 강의 (이 작업을 위해 학습한 강의)
|
|
|
|
- 없음.
|
|
|
|
### job-posting tie-ins (이 작업에서 파생된 글감)
|
|
|
|
- 후보: startup failure compact logging hardening은 블로그 글감으로 확장 가능하나, 이번 캡처에서는 별도 raw/blog-topic으로 분리하지 않음.
|
|
|
|
## 관련 일일 노트
|
|
|
|
- 없음.
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경: local verification only.
|
|
- **wiki 추출 대상**:
|
|
- `actually-implemented` 항목: startup failure stacktrace suppression implementation; startup settings/validator exception translation hardening; residual Spring Boot failure-analysis/context-cancellation log filter.
|
|
- `locally-verified` 항목: focused tests, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies`, DB down bootRun reproduction, invalid tracing sample rate bootRun reproduction.
|
|
- `prod-verified` 항목: 없음.
|
|
- **추출하지 않을 항목**:
|
|
- 전체 `./gradlew check`: `:sample-portfolio:test`의 `PortInUseException`으로 실패. 변경 범위와 분리해 raw error note에 기록.
|
|
- runtime background `log(..., ex)` stacktrace cleanup은 별도 future work.
|