The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
8.0 KiB
Markdown
132 lines
8.0 KiB
Markdown
# 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에만
|
|
정확히 한 번 발행한다.
|
|
5-1. V2 client와 V3 contract executor는 각각 자신의 logical execution에 대해
|
|
이 규칙을 만족한다. V3에서는 execution site가 `HttpExecutionObservation`
|
|
typed record 하나만 만들고, composition root의
|
|
`createHttpObservationProjector`가 유일한 projection authority다. observation은
|
|
arbitrary context map이 아니며 projector는 `route_id`, `operation_id`,
|
|
`operation`, `outcome`, `error_kind`, `http_status_group`,
|
|
`attempt_count_bucket`, `duration_bucket`만 사용한다. raw attempt count,
|
|
duration, status, URL, intent, key, input identity와 내부 `terminalReason`은
|
|
sink로 나가지 않는다. effect certainty가 운영상 필요해지면 `effect_certainty`
|
|
key와 닫힌 value policy를 contract·fixture·이 ADR에 동시에 추가한 뒤에만
|
|
전달한다.
|
|
5-2. caller cancellation과 scope fence는 API failure가 아니다. diagnostics는 한
|
|
번 남기고 `api.request.failed`는 발행하지 않는다.
|
|
5-3. `routeId`는 installed operation-executor 경계의 필수 입력이다. feature
|
|
gateway가 소유한 low-cardinality route identity를 URL에서 재구성하지 않는다.
|
|
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 경계다.
|
|
7-1. telemetry adapter lifecycle은 `ACTIVE | DISPOSED` 둘뿐이다. `dispose()`는
|
|
한 번만 전이하고 `pagehide` listener 제거, queue 비우기, scheduled callback
|
|
generation 무효화, in-flight sink `AbortController` abort를 모두 수행한다.
|
|
dispose 뒤 `emit()`은 no-op이고 새 flush는 스케줄되지 않으며, abort를 무시한
|
|
sink가 늦게 settle해도 post-dispose delivery state를 갱신하거나 재스케줄하지
|
|
못한다. 종료 중 drop telemetry를 재귀적으로 발행하지 않는다.
|
|
7-2. `flush()`는 active delivery promise를 join한다. 이미 진행 중인 flush가
|
|
있으면 같은 promise를 반환하므로 `await flush()`는 실제 settle을 뜻한다.
|
|
7-3. runtime `infrastructure.dispose()`는 diagnostics/state dependency를 파괴하기
|
|
전에 `telemetry.dispose()`를 먼저 호출한다.
|
|
7-4. queue/entry capacity는 construction-time 계약이다. `Number.isSafeInteger`가
|
|
아니거나 1 미만이거나 문서화된 ceiling(각각 `MAX_TELEMETRY_QUEUE`,
|
|
`MAX_DIAGNOSTIC_ENTRIES` = 10,000)을 넘으면 `TypeError`로 거절한다. NaN/Infinity가
|
|
조용히 eviction을 비활성화하는 경로를 남기지 않는다.
|
|
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.ts`
|
|
- 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.ts`
|
|
- composition: `src/bootstrap/runtime-adapters.ts`
|
|
|
|
## 검증
|
|
|
|
- `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와 요청 값 비노출을 검증한다.
|
|
- `tests/integration/http-execution-v3-observability.test.ts`는 V3 terminal
|
|
outcome이 실제로 closed allowlist를 통과하는지, terminal non-abort failure가
|
|
`api.request.failed`를 정확히 한 번 발행하는지, cancellation/scope fence가
|
|
발행하지 않는지, feature route ID가 executor 경계까지 보존되는지, sink 예외가
|
|
HTTP 결과를 바꾸지 못하는지를 검증한다.
|
|
- 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와 안전 계약을 함께 되돌리지 않는다.
|