feat: add diagnostics and telemetry runtime

This commit is contained in:
donghyeon-ka
2026-07-26 16:42:27 +09:00
parent 2fa0baa577
commit 5173b6c8d6
43 changed files with 1760 additions and 116 deletions
@@ -0,0 +1,97 @@
# VD-07: Diagnostics와 telemetry exporter 경계
- 상태: Accepted
- 결정일: 2026-07-26
- 적용 브랜치: `feature-frontend-diagnostics-telemetry-runtime`
- 재검토: 실제 운영 sink, consent가 필요한 analytics 또는 분산 tracing provider가
선정될 때
## 배경
기존 telemetry registry와 best-effort HTTP queue는 있었지만 운영 진단 record와
semantic event의 책임이 하나의 telemetry port에 섞여 있었다. boot, HTTP,
cache, storage, route와 release failure의 선언도 실제 production producer와
완전히 연결되지 않았다. 이 상태에서는 retry attempt마다 같은 사건을 발행하거나
raw URL, query, request body와 오류 객체가 queue에 들어갈 위험이 있다.
반면 skeleton 단계에는 실제 관측 vendor, endpoint의 운영 보안 정책, analytics
consent와 보존 기간이 결정되지 않았다. 특정 SDK를 기본 번들에 설치하는 것은
vendor 결정 전에는 안전한 기본값이 아니다.
## 결정
1. level 기반 운영 진단은 `DiagnosticsPort`, registry 기반 semantic event는
`TelemetryPort`로 분리한다. application은 두 port의 concrete adapter나
exporter SDK를 알지 못한다.
2. diagnostics의 level, event ID와 context key는 닫힌 registry/allowlist다.
telemetry도 event별 required/optional attribute와 value policy를 적용한다.
등록되지 않은 event·context·고카디널리티 값은 전송하지 않는다.
3. 기본 diagnostics adapter는 bounded in-memory evidence이고 telemetry는
설정이 없으면 true no-op이다. endpoint가 있을 때만 bounded oldest-drop
queue와 best-effort HTTP sink를 사용한다.
4. raw path/URL/query/body/response/storage value, credential, cookie, email,
stack과 오류 객체 전체는 context에 넣지 않는다. route ID, operation ID,
correlation ID, release ID, error kind, status/attempt/duration bucket만
허용한다.
5. HTTP logical execution은 success, retry recovery, terminal failure 또는
abort마다 `http.request.completed` diagnostics를 정확히 한 번 남긴다.
`api.request.failed` telemetry는 retry가 끝난 terminal non-abort failure에만
정확히 한 번 발행한다.
6. `app.boot.failed`, `ui.render.failed`, `release.mismatch.detected`,
`telemetry.delivery.dropped`를 production path에 연결한다. cache와 storage
실패는 diagnostics로 기록하되 raw key/value를 기록하지 않는다.
7. queue full, invalid event/context, serialization과 sink failure는 제한된
reason bucket으로 집계한다. drop observer의 failure는 다시 telemetry를
발행하지 않는 nonrecursive 경계다.
8. diagnostics와 telemetry failure는 제품 흐름, HTTP 결과, route transition,
storage/cache fallback과 React error surface를 바꾸지 않는다.
9. mount 전 bootstrap failure는 안전한 build/config/error kind만 별도 evidence로
만들며 untrusted error message, stack과 support 입력을 serialize하지 않는다.
10. 실제 error reporter, RUM, analytics나 tracing SDK는 같은 port 뒤의 외부
adapter로만 추가한다. SDK type과 event API를 application/feature/presentation
public contract에 노출하지 않는다.
## 실행 경계
```text
route/application/HTTP/cache/storage/bootstrap
-> typed DiagnosticsPort 또는 TelemetryPort
-> registry + allowlist + value policy
-> bounded memory/no-op 또는 best-effort HTTP adapter
-> 프로젝트가 선택한 외부 sink
```
- diagnostics contract: `src/contracts/diagnostics.ts`
- telemetry contract: `src/contracts/telemetry.js`
- application ports: `src/application/ports/diagnostics-port.ts`,
`telemetry-port.ts`
- bounded diagnostics: `src/adapters/diagnostics/bounded-diagnostics.ts`
- best-effort telemetry: `src/adapters/telemetry/best-effort-telemetry.js`
- composition: `src/bootstrap/runtime-adapters.js`
## 검증
- `check:diagnostics`는 모든 registry event에 production producer가 있는지와
source의 direct console/sensitive context 우회를 검사한다.
- negative source fixture는 direct console, unknown event와 raw context를 실제로
거절하며 TypeScript fixture는 잘못된 level/event ID를 거절한다.
- unit test는 allowlist, hostile/circular error, bounded diagnostics, no-op,
queue full, sink/observer failure와 pre-mount boot evidence를 검증한다.
- HTTP integration은 success, retry recovery, terminal failure와 abort의 producer
횟수, route/operation/correlation context와 요청 값 비노출을 검증한다.
- cache/storage/release/application/runtime test는 각 production wiring과
diagnostics failure isolation을 검증한다.
## 한계와 재검토 조건
기본 adapter는 운영 log 검색, source map 연계, session replay, distributed span,
analytics consent, sampling budget과 장기 보존을 제공하지 않는다. 실제 sink를
선정할 때 데이터 처리 지역, 보존 기간, consent, CSP, source map 접근 제어,
sampling과 비용 상한을 별도 결정해야 한다.
## Rollback
telemetry exporter는 runtime 설정을 끄거나 adapter wiring을 `noOpTelemetry`
바꾸어 독립적으로 제거할 수 있다. 이때도 `DiagnosticsPort`, registry,
redaction/value policy, producer-count와 negative fixture는 유지한다. 외부 SDK
문제로 application producer와 안전 계약을 함께 되돌리지 않는다.
@@ -33,14 +33,14 @@
특히 다음은 선행 해결이 필요하다.
RP-01~RP-08에서 TypeScript 도구 안전망, application runtime 주입,
RP-01~RP-09에서 TypeScript 도구 안전망, application runtime 주입,
query/mutation inbound adapter, HTTP 실행 계약과 executable route/release
recovery 계약, 제거 가능한 reference 수직 슬라이스, form/page, design system과
i18n 실행 경계 구현됐다. 현재 선행 해결
i18n 실행 경계와 diagnostics/telemetry production wiring은 구현됐다. 현재 선행 해결
대상은 다음과 같다.
1. diagnostics/telemetry 실제 producer 연결
2. registry evidence, 공급망과 optional adapter recipe 심화 게이트
1. registry evidence와 실제 compatibility diff
2. 공급망과 optional adapter recipe 심화 게이트
따라서 현재 상태를 “프론트 공통부가 모두 구현됐다”고 표현하면 범위가 과장된다.
더 정확한 표현은 다음과 같다.
@@ -66,7 +66,7 @@ i18n 실행 경계는 구현됐다. 현재 선행 해결
| 계층 의존 방향 | 부분 준비 | `.dependency-cruiser.cjs`, `src/application/ports` | inbound/outbound 명명과 `contracts` 소유권까지 집행 |
| application facade | 준비됨 | typed input/output catalog, provider, production composition test | feature input use case를 contribution으로 확장 |
| HTTP client | 준비됨 | path/search/body projection, runtime timeout/retry, abort/cleanup test | feature gateway 뒤에서 사용 |
| retry | 준비됨 | HTTP 단일 소유, runtime max attempts, Query retry off | RP-09에서 telemetry 연결 |
| retry | 준비됨 | HTTP 단일 소유, runtime max attempts, Query retry off, logical execution당 bounded diagnostics | terminal event 중복 방지 계약 유지 |
| 오류 모델 | 부분 준비 | error registry와 normalization 존재 | typed discriminated union과 계층별 mapper |
| 검증 | 준비됨 | runtime/API/route/form Zod parse 결과를 실행 경계에서 사용하고 domain invariant와 분리 | feature별 schema 소유권 유지 |
| 인증 연동 | 준비됨/프로젝트 선택 | opaque auth owner와 demo seam 존재 | 인증 방식별 recipe; 기본 token 저장소는 추가하지 않음 |
@@ -81,8 +81,8 @@ i18n 실행 경계는 구현됐다. 현재 선행 해결
| 아이콘 | 준비됨 | Lucide static vendor facade와 semantic icon/IconButton 접근성 계약 | 의미 icon 추가 시 bundle/접근성 기준 적용 |
| 폼 | 준비됨 | Zod 기반 local facade, error summary/focus, 422 allowlist, dirty/pending/conflict 정책 | 복합 form 요구가 생기면 VD-04 조건으로 vendor adapter 평가 |
| 국제화 | 준비됨 | 137-key typed catalog, locale provider, Intl formatter, safe fallback/alias, pseudo·RTL gate | 실제 locale·번역 승인은 프로젝트에서 연결 |
| logging/diagnostics | 미제공 | telemetry port는 있으나 logger 없음 | redaction이 적용된 diagnostics/logging 경계 |
| telemetry | 부분 준비 | registry, queue, redaction 존재 | HTTP·boot·cache·storage·route 사건에 실제 연결 |
| logging/diagnostics | 준비됨 | 별도 `DiagnosticsPort`, 8-event registry, allowlist, bounded/no-op adapter와 production producer | 실제 프로젝트의 remote sink는 port 뒤에서 선택 |
| telemetry | 준비됨/프로젝트 선택 | 5-event registry, bounded queue, redaction/value policy, boot·HTTP·render·release·drop producer | analytics/RUM/error vendor와 consent는 프로젝트에서 선택 |
| 비동기 상태 불변식 | 준비됨 | 배타적 typed overlay, stale latch, 실제 retry/conflict action | reference 화면에서 전체 상태 전시 |
| 단위·통합·E2E | 준비됨 | Vitest, RTL, MSW, Playwright 3엔진 | TS 테스트 검사, 실제 bootstrap 통합, 위험 시나리오 보강 |
| UI 회귀 검증 | 미제공 | axe/reflow는 있으나 visual baseline 없음 | Storybook 또는 동급 workshop과 시각 회귀 |
@@ -217,11 +217,14 @@ guard → browser navigation adapter의 1회 reload로 연결됐다. 일반 rend
failure는 이 경로에서 제외되고, 반복 실패·offline·malformed manifest·storage
실패는 지원 표면으로 fail-closed된다.
#### telemetry, registry, 공급망 gate의 실행 깊이가 부족하다
#### RP-09에서 diagnostics/telemetry 실행 깊이 보강
telemetry registry에는 여러 사건이 있지만 실제 production producer는 제한적이다.
boot, API attempt/final failure, auth recovery, storage/cache degradation, release/chunk
recovery를 registry 사건에 연결해야 한다.
`DiagnosticsPort``TelemetryPort`를 분리하고 boot, HTTP logical outcome,
render, cache, storage, route, release mismatch와 delivery drop을 production
producer에 연결했다. HTTP retry는 attempt별 terminal event를 발행하지 않고
logical execution 종료 시 한 번만 bounded outcome을 남긴다. allowlist와
value policy가 raw URL/query/body/storage value/error object를 거절하고 queue와
sink failure는 nonrecursive drop evidence로 제한된다.
registry/compatibility 검사는 다음까지 확장한다.
@@ -243,7 +246,7 @@ known vulnerability, license policy, SBOM/provenance를 pinned tool로 검사해
- token → primitive → pattern → template로 이어지는 디자인 시스템
- Lucide를 감싼 local icon registry와 `IconButton`
- typed message key, locale provider, formatter, pseudo-locale/RTL smoke
- redacted structured diagnostics/logger와 telemetry wiring
- RP-09에서 완료한 redacted structured diagnostics와 telemetry wiring 유지
- Storybook 또는 동급 isolated UI workshop
- Playwright visual baseline, shared MSW scenarios, built-dist E2E
- React Hooks, JSX accessibility, TanStack Query 관련 lint
@@ -380,7 +383,8 @@ bootstrap → React TSX → tests 순서로 이동한다.
- retry: `src/adapters/http/retry-policy.js`, 부분 준비
- API client: `src/adapters/http/client.js`, 부분 준비
- logger: 없음. telemetry와 분리하거나 diagnostics port로 합치는 결정 필요
- logger: `DiagnosticsPort`로 telemetry와 분리해 구현. closed event/level,
allowlist와 bounded/no-op adapter 제공
- token manager: 의도적으로 없음. opaque external auth owner가 credential을 소유
- error: `src/contracts/errors.js`와 HTTP normalization, 부분 준비
- validation: runtime/API Zod는 존재, route/form/domain 분리는 미완성
@@ -723,6 +723,28 @@ render와 release 사건을 실제 producer에 연결한다.
RP-09는 exporter를 제거하고 즉시 no-op adapter로 전환할 수 있어야 한다.
diagnostics port와 redaction test는 유지한다.
**구현 증거 (2026-07-26)**
- VD-07에서 level/event 기반 `DiagnosticsPort`와 semantic `TelemetryPort`
분리하고 bounded memory/no-op 또는 설정 기반 best-effort HTTP exporter를
채택했다.
- diagnostics 8종과 telemetry 5종의 registry, context/attribute allowlist,
고카디널리티 value policy, timestamp와 status/attempt/duration/queue bucket을
구현했다.
- boot, route, render, HTTP logical outcome, cache, storage, release mismatch와
telemetry drop을 production composition에 연결했다. HTTP는 success/recovery/
terminal/abort 각각 logical execution당 diagnostics 한 번, terminal
non-abort failure telemetry 한 번만 발행한다.
- bounded oldest-drop queue, drop reason 집계, sink/observer failure 격리와
nonrecursive delivery evidence를 구현했다. exporter가 없으면 network와 queue
side effect가 없는 true no-op이다.
- `check:diagnostics`, source negative fixture와 TypeScript negative fixture가
producer 누락, direct console, unknown event/context, raw URL/query/body/
credential 경계를 거절한다.
- unit/integration test가 circular/hostile error, pre-mount boot, queue/sink
failure, no-op, cache/storage/release producer, success/retry recovery/terminal/
abort 횟수와 reference route/operation/correlation context를 검증한다.
### 10. `feature-frontend-test-registry-evidence-hardening`
**목표**
@@ -779,22 +779,31 @@ Logger와 telemetry는 같은 것이 아니다.
- Telemetry: registry에 정의된 semantic event와 metric
- Error reporter: 예외 집계와 release correlation
기본 `Logger` output port는 safe context만 받는다.
VD-07에 따라 기본 구현은 임의 message 문자열을 받는 `Logger`가 아니라 닫힌
event ID와 safe context만 받는 `DiagnosticsPort`다.
```ts
export interface Logger {
debug(message: string, context?: SafeLogContext): void;
info(message: string, context?: SafeLogContext): void;
warn(message: string, context?: SafeLogContext): void;
error(message: string, context?: SafeLogContext): void;
export interface DiagnosticsPort {
record(input: {
level: DiagnosticLevel;
eventId: DiagnosticEventId;
context?: DiagnosticContext;
}): void;
}
```
개발 환경에는 redacted console adapter, production에는 allowlist 기반
remote adapter, 테스트에는 recording 또는 no-op adapter를 연결한다.
기본 runtime에는 bounded in-memory diagnostics와 설정 기반 best-effort
telemetry adapter, 테스트에는 recording 또는 no-op adapter를 연결한다.
direct `console`은 redaction 경계를 우회하므로 source gate가 거절한다.
민감정보 redaction은 각 호출자의 선의가 아니라 adapter와 contract에서
강제한다.
현재 production producer는 boot, logical HTTP outcome, cache, storage, route,
render, release mismatch와 telemetry delivery drop을 포함한다. HTTP terminal
telemetry는 모든 retry가 끝난 뒤 한 번만 발행하며 abort에는 발행하지 않는다.
raw path/query/body/error 대신 route/operation/correlation ID와
status/attempt/duration bucket만 전달한다.
## 16. Feature 경계와 removable reference feature
Reference feature는 단순 UI fixture가 아니라 다음 경로를 모두 실행해야
@@ -1223,9 +1232,10 @@ contract와 실패 분기를 우선한다.
- [ ] runtime timeout/retry 설정이 실제 HTTP transport에 반영된다.
- [ ] QueryClient와 feature query bridge가 실제 route에서 동작한다.
- [ ] session UI API와 credential attachment가 분리되어 있다.
- [ ] logger와 telemetry가 HTTP/render/storage/cache failure 경로에
- [x] diagnostics와 telemetry가 HTTP/render/storage/cache failure 경로에
연결된다.
- [ ] page lifecycle에서 필요한 telemetry flush/cleanup이 수행된다.
- [x] `pagehide`에서 bounded telemetry queue를 flush하고 adapter `dispose`
lifecycle listener를 정리한다.
### 26.3 HTTP와 validation
@@ -468,23 +468,31 @@ raw response body, stack, token, URL query, PII를 사용자 copy나 일반 log
### 7.3 diagnostics와 telemetry
현재 telemetry event contract와 별도로 개발·진단용 structured logger가 필요하다.
다음 두 설계 중 하나를 ADR로 결정한다.
1. `DiagnosticsPort`가 log/event/span을 내부 method로 구분
2. `LoggerPort``TelemetryPort`를 분리
VD-07에서 level/event 기반 `DiagnosticsPort`와 semantic
`TelemetryPort`를 분리했다. diagnostics는 8개 event ID와 safe context
allowlist를, telemetry는 event별 required/optional attribute와 value policy를
사용한다.
공통 요구:
- log level과 event key는 닫힌 union
- attribute allowlist와 중앙 redaction
- dev adapter는 console을 사용하되 동일 redaction 적용
- production adapter는 provider SDK를 감싸며 앱 코드는 SDK를 import하지 않음
- 기본 adapter는 bounded memory/no-op이고 endpoint가 있을 때만 best-effort
HTTP queue를 사용
- production provider SDK를 추가할 때도 port 뒤에서 감싸며 앱 코드는 SDK를
import하지 않음
- 오류 객체 전체를 그대로 serialize하지 않음
- trace ID/build ID/route ID/operation ID를 허용된 범위에서 연결
- logging failure가 제품 flow를 실패시키지 않음
- consent가 필요한 analytics와 essential diagnostics를 분리
HTTP는 route/operation/correlation ID를 logical execution context로 생성하고
success/recovered/failed/aborted 종료 시 diagnostics를 한 번만 기록한다.
terminal non-abort failure만 telemetry를 한 번 발행한다. cache/storage와 boot
producer는 raw key, value, message, stack을 버리고 error kind와 bounded
operation만 남긴다. queue full과 sink failure는 제한된 reason bucket이며 drop
observer가 실패해도 재귀 발행하지 않는다.
### 7.4 locale, message와 표시 값
RP-08부터 locale은 presentation-owned React context다. server/application
@@ -410,6 +410,8 @@ Page / AsyncSurface / Form pattern
- filter 순서가 달라도 canonical key가 같다.
- route unmount 또는 superseded input에서 request를 abort한다.
- aborted request는 terminal error나 telemetry failure로 오분류되지 않는다.
- success와 retry recovery는 terminal failure telemetry를 만들지 않고,
exhausted retry는 logical execution당 한 번만 발행한다.
- offline/paused와 loading을 구분한다.
- 401 복구는 한 번만 수행한다.
- 재로그인 후 허용된 operation만 다시 실행한다.
@@ -1283,7 +1285,8 @@ CI registry에 추가한다.
- [ ] high-risk module branch 목표를 충족한다.
- [ ] 신규 코드의 미검증 branch에 승인 없는 예외가 없다.
- [ ] bundle/performance budget을 통과한다.
- [ ] telemetry/error output에 민감 정보가 없다.
- [x] diagnostics/telemetry output의 allowlist, value policy와 negative fixture가
raw URL/query/body/storage value/error object를 거절한다.
- [ ] 관련 gate와 evidence registry가 갱신되었다.
## 19. 금지 패턴