Files
tech-log-frontend/docs/architecture/decisions/VD-07-diagnostics-and-telemetry-exporter.md
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
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>
2026-08-15 12:04:58 +09:00

8.0 KiB

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에 노출하지 않는다.

실행 경계

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와 안전 계약을 함께 되돌리지 않는다.