Files
llm-wiki/raw/branch-notes/feature-log-management-contract.md
T

431 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-log-management-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-003
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-003
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
branch: feature-log-management-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]
tags: [branch, ca-skeleton, logging, observability]
created: 2026-05-21
target_merge:
status_label: implemented
last_pass: 2026-06-14 (Phase C2 전면 구현 완료 — DRIFT-1~6 + sampling 전부 actually-implemented, locally-verified. 사용자 결정 2건: Q1=dependency level 어댑터종류 분리(optional fail-open WARN / core HTTP ERROR), Q2=full HMAC pseudonymization. 구현: (1)DRIFT-2 Layer1 masking = `LogMaskingPatterns`(SSOT 정규식 catalog) → `SecretMaskingJsonGeneratorDecorator`(JSON encoder, `MaskingJsonGeneratorDecorator` — `%replace`는 LogstashEncoder가 PatternLayout 우회하므로 부적합) + `SecretMaskingMessageConverter`(`%maskedMsg`, local pattern). (2)DRIFT-1/D10 = `logback-spring.xml` `<springProfile name="local,dev">` PatternLayout vs `!local & !dev` JSON. (3)DRIFT-3 = uri_template. (4)DRIFT-4 = `OutboundDependencyLogger` snake_case + dependency_type + WARN(Q1) + 5 callers. (5)DRIFT-5 = `MetricsAsyncAppender`(AsyncAppender 서브클래스, `Metrics.globalRegistry`로 `log.appender.dropped.total` 발행; discardingThreshold>queueSize 트릭으로 결정론적 테스트). (6)DRIFT-6 = `UserPrincipalPseudonymizer`(application-core 포트) + `HmacUserPrincipalPseudonymizer`(adapter-identifier, HMAC-SHA-256 hex) + `PseudonymizationConfig`/`PrivacySettings`(app-bootstrap, salt=`APP_PRIVACY_PSEUDONYMIZATION_SALT`) + RequestLoggingFilter 배선. sampling = `SamplingTurboFilter`(≤INFO rate, WARN/ERROR 100%). 리뷰 체인 3단계 ALL PASS(architect/spec/quality ready). 가드레일 green(`verifyCleanArchitectureDependencies`/`verifyPublicPathSnapshot`/touched-module tests). **잔존**: (a)D9 전용 audit appender는 의도적 보류(생산자 부재 + retention은 data-retention 소유 + §Decisionized Work Items 비포함). (b)`./gradlew :app-bootstrap:test`에 pre-existing 실패 1건 — `outbound_adapter_method_returns_only_domain_or_primitives`(`OutboundHttpSettings.circuitBreaker()/retry()`, commit d702572 도입, clean tree에서도 실패 — 본 작업과 무관). 사용자가 직접 커밋.)
contract_packet_sha256: 214a476351c279e55c3afdd170db0165aa04000a706ff51a758f96a11dcc791c
---
# branch: feature-log-management-contract
> Layer: `raw/branch-notes/` — structured log schema와 로그 금지 정책을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
[[raw/project-notes/ca-skeleton-operational-contract]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: log field·masking contract와 verification test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1` | foundation이 envelope schema와 error.category enum의 단일 owner다 | structured log field와 error category correlation 및 masking contract에 적용한다 | [[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 -->
## 목표
운영자는 exception class보다 어떤 operation, dependency, retryable 여부, trace/correlation 정보가 필요한지 봅니다. 이 branch는 skeleton의 log contract를 확정합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- JSON log schema.
- request/application/dependency/security/audit log 구분.
- PII/secrets/token/password/body 로그 금지.
- level 기준.
- sampling 정책.
- async appender overflow 기준.
- stdout/file logging 기준.
- profile별 console encoder 포맷 — local/dev=human-readable pattern, staging/prod=JSON (D10).
### 제외 범위
- 실제 로그 수집 플랫폼 구축.
- Grafana/ELK 대시보드 구현.
- business metric 정의.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/log-logback-mask-pattern-converter-official.md]] | Logback PatternLayout / custom converter spec |
| [[raw/official-docs/log-ecs-schema-elastic-official.md]] | 자체 schema와 ECS 매핑 가능성 평가 |
| [[raw/official-docs/log-otel-log-data-model-spec.md]] | trace 자동 correlation 장점이나 OTLP collector 추가 deploy 필요 |
| [[raw/official-docs/container-stdout-logging-12factor-official.md]] | D4: production logging = stdout JSON default; file logging = local/dev only — Twelve-Factor App Factor XI ("Logs") 직접 근거 (LOG-12F-C1 ~ C4) |
| [[raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official.md]] | D4 — AWS ECS 환경 구체화: awslogs 드라이버가 컨테이너 stdout/stderr 를 Docker 경유로 CloudWatch Logs 전달 — 앱 내 별도 shipper 불필요 (LOG-ECS-AWSLOGS-C1) |
| [[raw/official-docs/k8s-logging-architecture-kubernetes-official]] | D4 — Kubernetes 공식 문서: stdout/stderr 직접 출력이 가장 권장(C1), streaming sidecar 는 stdout 불가 앱 폴백(C2), file→stdout 이중 경로 디스크 2배 경고(C3), 단일 파일 앱 `/dev/stdout` 권장(C4), kubelet 기본 rotation 10Mi/5files + 장기 retention 불가(C5) |
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-A: Log management)
### 채택 결정 + 뒷받침
- 결정: **structured JSON log + Logback masking converter (Layer 1 SSOT) + prod 10% INFO sampling**.
- 뒷받침 source:
- [[raw/official-docs/log-logback-mask-pattern-converter-official.md]] — Logback PatternLayout / custom converter spec. Layer 1 (final encoder 직전) 위치가 MDC/message/stack trace 전부 통과시키는 가장 넓은 catch-net임을 spec으로 확인.
- [[raw/official-docs/log-ecs-schema-elastic-official.md]] — 자체 schema와 ECS 매핑 가능성 평가. ca-tmpl 필수 8-10 field가 ECS와 1:1 매핑 가능 (예: `traceId``trace.id`).
### 검토 대안 + source
- 대안 1 — **ECS schema 직접 채택**: [[raw/official-docs/log-ecs-schema-elastic-official.md]]. 업계 표준이나 field 폭주(수백 개) 위험.
- 대안 2 — **OpenTelemetry log signal**: [[raw/official-docs/log-otel-log-data-model-spec.md]]. trace 자동 correlation 장점이나 OTLP collector 추가 deploy 필요. 2024 GA로 ecosystem maturity 낮음.
### 비교 핵심 1줄
자체 schema는 **minimal core 강제 + JVM 친화 + stdout 단순화**가 강점, ECS는 vendor 호환성, OTel log signal은 미래 통합. skeleton 단계에서는 자체 schema + Logback 채택이 운영 비용 최소.
## TODO
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Sampling Policy (final)" / "Redaction Layer SSOT" / "Audit Log Retention / Compliance" / "Log Type별 필수 필드" / "Decisionized Work Items" 참조. JSON log 필수 필드/log type 구분/redaction(PII/secrets/token/body)/level/sampling/async overflow/stdout-vs-file 모두 표 또는 결정 라인으로 반영됨. structured log field 존재 테스트는 `feature-contract-verification-test-suite`로 위임. 잔존 TODO 없음.
## 진행 중 메모
- log body는 기본 금지. allowlist redaction 없이 켜지지 않아야 합니다.
## 결정 사항 (decisions)
- 2026-05-21: JSON log를 기본 운영 포맷으로 둠.
- 2026-05-22: MDC/log key naming의 SSOT는 `feature-operational-error-observability-foundation`. 이 branch는 log type, level, sink, overflow policy를 소유.
- 2026-05-22: domain layer logger는 금지. domain invariant violation의 reason code는 application layer에서 client-safe diagnostic log로 변환.
- 2026-05-22: production logging은 stdout JSON default, file logging은 local/dev only.
- 2026-05-22: log sampling(prod 10%) > trace sampling(prod 1%)는 의도된 분리. log는 운영 진단에 trace보다 자주 필요(특히 trace_id 없는 단순 query). log-only correlation은 request_id로 추적. distributed-tracing branch와 정합.
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
> `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
> `Supporting Claims` 는 `raw/<category>/<slug>.md#<CLAIM-ID>` 형식으로 연결한다.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | JSON log를 기본 운영 포맷으로 둠 (structured JSON + Logback masking converter Layer 1 SSOT) | `raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C1`, `raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C3`, `raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C5` | `official-vendor-doc` (Logback PatternLayout / custom converter / `%replace` 정규식 치환 spec) | Logback 의 built-in PII masking converter 부재는 본 페이지 인용으로 직접 증명되지 않음 (`LOG-LBK-C5` Usage Boundaries). regex false negative 가능 (Base64 token 등) — 정규식 catalog 별도 운영 검증 필요. **구현 현황: JSON encoder(`LogstashEncoder`)는 actually-implemented, masking converter(Layer 1)는 미구현 → DRIFT-2** |
| D2 | MDC key naming 의 SSOT 는 `feature-operational-error-observability-foundation` (consume only) | `raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C4` (`%mdc{key}` converter spec — MDC 가 thread-local 임을 확인) | `official-vendor-doc` | MDC 가 async/reactive thread 전환 시 자동 전파된다는 뜻 아님 (`LOG-LBK-C4` Does not prove). Reactive boundary 별도 propagation 필요 |
| D3 | domain layer logger 금지 — application layer 에서 client-safe diagnostic log 로 변환 | UNSUPPORTED_DECISION (외부 official-standard / official-vendor-doc 직접 근거 없음 — clean architecture / DDD 일반 원칙에 가까운 ca-tmpl 내부 정책) | N/A | 외부 raw source 로 직접 뒷받침되지 않으므로 면접/외부 공개 시 "내부 정책" 으로만 표현. wiki 추출 시 `wiki/concepts/clean-architecture-*` 류 별도 근거 필요. ArchUnit `domain-core` logger import 금지 rule 로 강제 가능 (정적 검증) |
| D4 | production logging stdout JSON default, file logging local/dev only | `raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C1` (앱은 로그 라우팅·저장 직접 관리 금지 + logfile 쓰기·관리 시도 금지), `raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C2` (각 프로세스는 이벤트 스트림을 unbuffered stdout 에 기록), `raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C3` (staging/prod 에서 실행 환경이 스트림 캡처·라우팅 담당), `raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official.md#LOG-ECS-AWSLOGS-C1` (AWS ECS + awslogs 드라이버가 컨테이너 stdout/stderr 를 Docker 경유 CloudWatch Logs 로 전달 — 앱 내 별도 shipper 불필요), `raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official.md#LOG-ECS-AWSLOGS-C2` (awslogs 캡처 대상 = stdout/stderr — 파일 로그는 별도 처리 필요) | `official-standard` (Twelve-Factor App Factor XI) + `official-vendor-doc` (AWS ECS awslogs) | stdout 로그 포맷(JSON vs plain-text)은 Factor XI 가 규정하지 않음 — D1(JSON 기본 포맷)은 별도 `log-logback-mask-pattern-converter-official` / `log-ecs-schema-elastic-official` 근거. `FILE_ENABLED=true` 가 local/dev 에만 적용되는지 환경 게이트 검증 필요. AWS ECS 이외 환경(K8s, on-prem)에서 stdout 수집 체인은 별도 검증 필요 — `LOG-ECS-AWSLOGS-C1` 은 AWS-vendor 특화 근거. **구현 현황: `FILE_ENABLED` property(default false) toggle 로 actually-implemented** |
| D5 | log sampling (prod 10%) > trace sampling (prod 1%) 분리 — log 가 운영 진단에 trace 보다 자주 필요 | UNSUPPORTED_DECISION (sampling 비율 분리는 운영 trade-off; 인용된 official-docs 3개 어디에도 정량 권장값 없음) | N/A | log/trace sampling 비율 의 정량 권장 표준 부재. 본 비율은 운영 가정 — 운영 후 재조정 필요. distributed-tracing D6 (trace 1%) 와 상호 확인된 짝. **구현 현황: sampling 로직 미구현 → DRIFT(planned)** |
| D6 | ECS schema 직접 채택 거부 (자체 schema 유지, ECS 와 매핑은 보존) | `raw/official-docs/log-ecs-schema-elastic-official.md#LOG-ECS-C1` (ECS 의 정의 + Usage Boundaries: ECS 가 비-Elastic sink 의 공식 표준이라는 뜻은 아님) | `official-vendor-doc` | ca-tmpl 자체 schema 가 ECS 보다 우월하다는 결론 아님. 단지 minimal core 강제 + 자체 운영 비용 최소화의 trade-off |
| D7 | OpenTelemetry Log signal 직접 emit 거부 (ecosystem maturity / collector deploy 비용) | `raw/official-docs/log-otel-log-data-model-spec.md#LOG-OTEL-C1`, `raw/official-docs/log-otel-log-data-model-spec.md#LOG-OTEL-C5` (OTel log spec + W3C trace context 정합) | `official-standard` | 본 spec 자체는 stdout JSON 보다 열등하다고 말하지 않음 (`LOG-OTEL-C1` Does not prove). 운영 비용 평가는 ca-tmpl 내부 판단 |
| D8 | INFO 이하만 sampling 대상, WARN/ERROR 100% 보장 | UNSUPPORTED_DECISION (인용된 official-docs 에 sampling 정책 직접 근거 없음 — 운영 best practice 일반론) | N/A | WARN/ERROR 100% 보장은 운영 관행이나 표준 spec 인용 없음. ca-tmpl 내부 정책으로만 표현. **구현 현황: AsyncAppender `discardingThreshold`(≤INFO drop, WARN/ERROR 보존)는 actually-implemented; 비율 sampling 은 미구현** |
| D9 | audit log = append-only file appender + remote forwarding + immutable | UNSUPPORTED_DECISION (audit log retention SSOT 는 `data-retention-privacy-contract` consume — 본 branch 인용 자료에 직접 근거 없음) | N/A | 외부 audit log immutability 표준 (예: SOX / PCI DSS) 별도 raw source 필요. retention 수치는 [[raw/branch-notes/feature-data-retention-privacy-contract]] SSOT (audit 365일). 본 branch 는 *형식* 만 owns. **구현 현황: 전용 audit appender 미구현 → planned** |
| D10 | local/dev profile 콘솔(stdout) 출력 = human-readable PatternLayout encoder, JSON(`logstash-logback-encoder`)은 staging/prod 전용 — `logback-spring.xml``<springProfile>` 분기로 encoder 선택. **D1(JSON=기본 운영 포맷)을 profile 축으로 정밀화** (운영=staging/prod 은 JSON 유지, local console 만 가독성 예외). D4(sink routing)와 보완 관계 — D4=*어디로 보낼지*, D10=*local console 을 어떤 포맷으로 찍을지* | `raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C1` (PatternLayout 은 logging event 를 printf 류 human-readable String 으로 출력하며 JSON encoder 와 **별개 메커니즘**임을 spec 으로 확인 — profile별 encoder 전환의 직접 근거) | `official-vendor-doc` (mechanism only) | `<springProfile>` 분기 메커니즘 자체 + "local=human-readable 이 DX 에 유리" rationale 은 운영 trade-off (D4/D5 동류; `<springProfile>` element 의 raw source 부재 → UNSUPPORTED operational 가정, 면접/외부 공개 시 "내부 DX 정책"으로 표현). Redaction 영향 없음: local PatternLayout 에도 동일 `%replace` 마스킹 converter(`LOG-LBK-C5`) 적용 가능 → 가독성 전환이 secret 노출로 이어지지 않음. **구현 현황: 실제 `logback-spring.xml` 은 전 profile JSON `LogstashEncoder` 사용, `<springProfile>` 분기·PatternLayout 부재 → D10 은 planned, DRIFT-1** |
## MDC Key Consumption
이 branch는 MDC/log key 이름표를 다시 작성하지 않습니다. `feature-operational-error-observability-foundation`의 "MDC Key Standard (final)" 표를 그대로 consume 합니다. 키 추가/변경이 필요하면 foundation branch의 registry를 먼저 갱신해야 합니다.
- **Core 6 키 (foundation owner)**: `request_id`, `trace_id`, `span_id`, `correlation_id`, `tenant_id`(tenant-context-policy owner), `user_principal``mdc-keys.yaml` 에 등록, snake_case 강제.
- **본 branch owner log-field 키 (13개, `mdc-keys.yaml` `owner_branch: feature-log-management-contract`)**: `operation`, `method`, `status`, `duration_ms`, `dependency_name`, `dependency_type`, `outcome`, `error_code`, `event_type`, `source_ip_anon`, `actor`, `action`, `target`. 각 row 의 `required_test: contract-verification:log-fields`.
- 실제 코드 노출 키(`logback-spring.xml` `includeMdcKeyName`): `trace_id`, `span_id`, `request_id`, `correlation_id`, `user_principal` (actually-implemented). 본 branch owner 13 키는 registry 등록되었으나 encoder 자동노출 목록엔 미포함 — log type 별 structured argument 로 주입 (planned 정합).
## Sampling Policy (final)
INFO 이하만 sampling 대상. WARN/ERROR는 항상 100% 보장.
| profile | high-traffic endpoint | normal endpoint |
|---------|------------------------|-----------------|
| prod | 10% | 100% |
| staging | 100% | 100% |
| dev/local | 100% | 100% |
async appender overflow default: drop oldest INFO/DEBUG with counter metric (`log.appender.dropped.total`). WARN/ERROR drop 금지.
> ⚠️ 구현 정합 메모(2026-06-13): 실제 `AsyncAppender`(queueSize=512, discardingThreshold=20, neverBlock=false)는 **유입(newest) ≤INFO 이벤트를 drop** 하며(잔여 capacity ≤ threshold 시), 완전 포화 시 caller thread 가 **block**(neverBlock=false). "drop oldest" 표현은 Logback 동작과 불일치 — Audit DRIFT-5. 비율 기반 prod 10% sampling 은 TurboFilter 미구현(planned). `log.appender.dropped.total` 은 metrics.yaml 등록되었으나 AsyncAppender 가 Micrometer counter 미발행 → wiring planned.
## Redaction Layer SSOT
- Layer 1 (primary): Logback masking converter (PatternLayout 단계). 모든 ERROR/WARN 진입 시 token/password/auth header pattern을 ****로 치환.
- Layer 2 (secondary): Jackson `@JsonSerialize(using=MaskingSerializer.class)` for known PII fields in DTO.
- Layer 3 (defensive): request body capture filter — allowlist 없이는 capture 자체 금지.
- Layer 1이 SSOT. Layer 2/3는 보완. allowlist 미정 시 capture forbidden(default deny).
> ⚠️ 구현 정합 메모(2026-06-13): Layer 1~3 모두 **미구현(`documented-only`/`planned`)** — `logback-spring.xml` 에 masking converter(`%replace`) 없음. 현재 secret-누출 방지는 *by construction* (logger 가 body/payload 인자를 받지 않음 — `OutboundDependencyLogger`). governing doc `observability-log-metric-trace-runbook` L108 도 "masking 정책은 문서에만 존재" 로 `documented-only` 명시. Audit DRIFT-2. `user_principal` pseudonymization *알고리즘* SSOT 는 [[raw/branch-notes/feature-data-retention-privacy-contract]] 로 **확정** (pseudonymization key = HMAC-SHA-256 + 90d salt rotation 결정 + pseudonymized id ↔ original id 변환표 owns). foundation §2 Q12 가 본 branch/security-operational-baseline 도 후보로 열거했으나 알고리즘 결정은 data-retention 보유 — 본 branch 는 security/audit log 에 *pseudonymized 형태로만 기록* 계약만 owns. DRIFT-6 (코드는 raw `idpUserId()` 기록).
## Audit Log Retention / Compliance
- audit log retention 정책은 data-retention-privacy-contract SSOT consume. 본 branch는 audit log의 **형식**(append-only file appender + remote forwarding)만 결정.
- append-only file appender + remote forwarding (Loki/CloudWatch).
- audit log는 변경/삭제 금지(immutable).
## Log Type별 필수 필드
| log type | 필수 필드 |
|----------|-----------|
| request | request_id, trace_id, method, uri_template, status, duration_ms |
| dependency | dependency_name, dependency_type, duration_ms, outcome, error_code (실패 시) |
| security | event_type, user_principal (pseudonymized), source_ip (anonymized — last octet zeroed) |
| audit | actor, action, target, before_hash, after_hash, occurred_at |
| application | 자유 형식, 단 mandatory MDC keys (foundation 표) 유지 |
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --- | --- | --- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test |
| --- | --- | --- | --- | --- |
| key names | foundation registry consumes | external ECS mapping table | branch-local MDC names | log field test |
| domain diagnostics | application logs translated reason code | domain exception has safe enum | domain logger | forbidden import test |
| sink | prod stdout JSON | local file logging | prod file-only logs | profile log test |
| overflow | bounded async appender + drop/blocks documented | sync logging for small apps | unbounded queue | overflow policy test |
| console format | local/dev=human-readable pattern, staging/prod=JSON encoder (D10) | `<springProfile>` 분기 in `logback-spring.xml` + local pattern 에 `%replace` 마스킹 유지 | local 에서 JSON 강제 / prod·staging 에서 pattern 강제 / 마스킹 없는 local pattern | profile별 console encoder 형식 test |
## 구현 가이드
> *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세. ca-tmpl `src/` 코드를 anchor 로 쓰되, 코드로 확인 안 된 것은 `planned`/`documented-only` 로 표기. 모든 sub-section 은 본 branch 의 Decision ID + Supporting Claim ID 를 reference (R1). 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` 라벨 (R2).
### 1. Logback appender topology
> **Trace**: D1(JSON 기본 운영 포맷, `LOG-LBK-C1`) + D4(stdout default / file local-dev). 실제 구현: `src/app-bootstrap/src/main/resources/logback-spring.xml`.
>
> - **UNSUPPORTED_IMPL_DECISION**: `queueSize=512` / `discardingThreshold=20` 의 *수치* 는 운영 가정 (Logback default queueSize=256 에서 상향) — raw source 없음, 운영 후 재조정.
| 요소 | 구현 (코드 확인) | 등급 | Trace |
|---|---|---|---|
| JSON_CONSOLE | `ch.qos.logback.core.ConsoleAppender` + `net.logstash.logback.encoder.LogstashEncoder` (`app-bootstrap/build.gradle:76` `logstash-logback-encoder:8.0`) | actually-implemented | D1 / LOG-LBK-C1 |
| JSON_FILE | `RollingFileAppender` + `SizeAndTimeBasedRollingPolicy`(maxSize/maxHistory/totalSizeCap), **conditional `FILE_ENABLED`(default false)** | actually-implemented | D4 |
| async wrap | `ch.qos.logback.classic.AsyncAppender` (ASYNC_CONSOLE/ASYNC_FILE) queueSize=512, discardingThreshold=20, neverBlock=false | actually-implemented | Sampling Policy overflow / D8 |
| MDC 노출 | encoder `includeMdcKeyName`: trace_id, span_id, request_id, correlation_id, user_principal | actually-implemented | D2 (consume foundation) |
| 설정 바인딩 | `<springProperty>``ca-skeleton.logging.*``LoggingSettings.java` (`@ConfigurationProperties`, warn-and-default: bad value → warn 로그 + default, startup 실패 안 함) | actually-implemented | D4 |
### 2. Console encoder per profile (D10) — 미구현
> **Trace**: D10 (local/dev=human-readable PatternLayout, staging/prod=JSON via `<springProfile>`), `LOG-LBK-C1`.
>
> - **UNSUPPORTED_IMPL_DECISION + DRIFT-1**: 실제 `logback-spring.xml` 은 전 profile `LogstashEncoder`(JSON) 사용 — `<springProfile>` 분기 / PatternLayout 부재(`src/` grep 0건). → D10 은 `planned`. 구현 시 `<springProfile name="local,dev">` 안 PatternLayout `<encoder>`, `<springProfile name="staging,prod">` 안 LogstashEncoder 로 분기 + local PatternLayout 에도 `%replace` 마스킹 동일 적용 (가독성 전환이 secret 노출로 이어지지 않게).
### 3. Request log type (adapter-web)
> **Trace**: Log Type별 필수 필드(request) + D2 consume. 구현: `src/adapter-web/.../filter/RequestLoggingFilter.java` (actually-implemented; `RequestLoggingFilterTest` locally-verified per governing doc L42).
>
> - **UNSUPPORTED_IMPL_DECISION + DRIFT-3**: 필수필드 표는 `uri_template`(low-cardinality route) 요구하나 코드는 `req.getRequestURI()`(raw path, high-cardinality) 기록 (`RequestLoggingFilter.java:64`). 정합하려면 `RateLimitKeyResolver.java:48` 와 동일하게 `HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE` 사용 → `planned`.
- 현재 동작(2026-06-14 actually-implemented): `log.info("http_request method={} uri_template={} status={} duration_ms={}")` at INFO (`OncePerRequestFilter`). `HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE` 로 route template 추출(fallback: `getRequestURI()` for 404s). `UserPrincipalPseudonymizer` 생성자 주입 — `user_principal` MDC 에는 `pseudonymizer.pseudonymize(user.idpUserId())` 결과만 기록(null 반환 시 미기록). 기존 `path=` 필드명은 `uri_template=` 로 변경(DRIFT-3 해소). 2개 신규 테스트 추가(`logs_uri_template_not_raw_path_when_handler_mapping_attribute_set`, `puts_pseudonymized_user_principal_on_mdc_not_raw_id`) — locally-verified via `./gradlew :adapter-web:test` (BUILD SUCCESSFUL).
- MDC 주입: `request_id`(inbound `X-Request-Id` sanitize 또는 UUID), `correlation_id`(`X-Correlation-Id`), `trace_id`(= request_id mirror, Micrometer Tracing 공급 전까지 — foundation/tracing D7), `user_principal`(pseudonymized via `UserPrincipalPseudonymizer` — DRIFT-6 해소, raw `idpUserId()` 미기록).
- inbound id 보안: `HeaderSanitizer.sanitize(.., MAX_ID_LENGTH=200)` — CR/LF·제어문자 strip + length cap (CWE-117, foundation D14). finally 에서 MDC 전부 remove.
### 4. Dependency log type (adapter-outbound)
> **Trace**: Log Type별 필수 필드(dependency) + 테스트 계약. 구현: `src/adapter-outbound/.../support/OutboundDependencyLogger.java` (actually-implemented).
>
> - **UNSUPPORTED_IMPL_DECISION + DRIFT-4**: (a) 필수필드 표(`dependency_name/dependency_type/duration_ms/error_code`) vs 코드 필드(`dependency/operation/outcome/correlationId/error`) 불일치; (b) 테스트 계약 "5xx와 dependency failure가 ERROR 이하면 실패" vs 코드 `logFailure`= **WARN**(optional fail-open adapter) — *core* dependency 실패 vs *optional* fail-open 실패의 level 정책 분리가 노트에 미명시; (c) message 안 `correlationId=`(camelCase) vs MDC snake_case SSOT 불일치. → 모두 `planned` 정합 (또는 결정 명시).
- 현재 동작: `logSuccess`= DEBUG (`dependency operation outcome correlationId`), `logFailure`= WARN (`dependency operation outcome correlationId error="class: message"`). payload/recipient/PII 인자 자체를 받지 않음 (*by construction* PII 안전 — Redaction Layer 보완).
### 5. Redaction layers — Layer 1 미구현
> **Trace**: Redaction Layer SSOT (Layer 1/2/3) + D1 (`LOG-LBK-C5`).
>
> - **UNSUPPORTED_IMPL_DECISION + DRIFT-2/DRIFT-6**: Layer 1 masking converter(`%replace`) **미구현** (`src/` grep 0건; `AuthErrorResponseWriter.java:25-30` 주석 "a full log-masking filter is delegated to `feature-secrets-config-source-contract` / log-management"). 현재 안전성 = *by construction*. → Layer 1~3 `planned`. `user_principal` 은 raw `idpUserId()` 기록(`RequestLoggingFilter.java:81`) — pseudonymization 미적용(DRIFT-6); 알고리즘 SSOT 는 [[raw/branch-notes/feature-data-retention-privacy-contract]] (HMAC-SHA-256 + 90d salt rotation), 본 branch 는 *pseudonymized 형태로만 기록* 계약만 owns.
### 6. Sink routing & sampling
> **Trace**: D4 (sink) + D5/D8 + Sampling Policy(final).
>
> - **UNSUPPORTED_IMPL_DECISION**: sink toggle `FILE_ENABLED`(default false=stdout only)는 actually-implemented. 비율 sampling(prod 10% INFO, WARN/ERROR 100%)은 logback 에 `TurboFilter`/sampler 부재(grep) → `planned`. 구현 시 level<WARN + high-traffic logger 대상 `ch.qos.logback.classic.turbo.TurboFilter` 또는 marker 기반 sampler + `log.appender.dropped.total` Micrometer wiring (현재 미배선, DRIFT-5).
## 엣지·실패·의존
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거.
- **실패·엣지 경로**:
- **logging config 오설정 → silent fallback**: `LoggingSettings` warn-and-default — 잘못된 값(blank path, timezone, queueSize≤0)은 warn 로그 후 default 로 진행, startup 실패 안 함. profile 오설정이 prod 에서 잘못된 encoder/sink 를 silently 선택해도 부팅됨 → Claims To Verify 의 profile별 첫 로그 라인 assert 로 검출.
- **async queue 포화**: 잔여 capacity ≤ discardingThreshold(20) → 유입 ≤INFO drop, WARN/ERROR 보존; 완전 포화 + neverBlock=false → caller thread **block**(latency spike 위험). (노트 "drop oldest" 표현 부정확 — DRIFT-5).
- **regex masking false negative**: Base64/URL-encoded token 등은 정규식 우회 가능 — Layer 1 구현 후 정규식 catalog 운영 검증 (Claims To Verify).
- **MDC async/reactive 경계 미전파**: `LOG-LBK-C4` (thread-local) → `@Async`/reactive 에서 `request_id`/`trace_id` 유실. TaskDecorator 또는 Micrometer Observation propagation 필요 (Claims To Verify).
- **file appender prod 오활성화**: `FILE_ENABLED=true` 가 prod 에 새면 stdout+file 이중 sink + 디스크 fill (D4 위반) — env 검증 또는 prod profile 강제 off 필요.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-operational-error-observability-foundation]] D11/D19 (MdcKeys snake_case SSOT) **consume** — foundation 이 키 rename/추가 시 본 branch `MdcKeys.java` + encoder `includeMdcKeyName` + Log Type 필드표 동시 갱신 필요. foundation §Coverage 가 "structured JSON Logback + masking/redaction + log sampling | delegated → 본 branch" 로 위임 명시.
- [[raw/branch-notes/feature-data-retention-privacy-contract]] (retention 수치 + PII field allowlist) **consume** — 본 branch 는 *형식* 만 owns(D9). retention 수치(application 30일 / security 180일 / audit 365일)는 그 branch Retention by Profile 표가 SSOT.
- [[raw/branch-notes/feature-distributed-tracing-contract]] D6 (trace sampling prod 1%) — D5 log sampling 10% 의 의도된 짝(양 노트 L74 상호 확인). `trace_id` 는 tracing/foundation owner, 본 branch 는 Micrometer 공급 전까지 `request_id` mirror.
- [[raw/branch-notes/feature-data-retention-privacy-contract]] — `user_principal` pseudonymization *알고리즘* SSOT (HMAC-SHA-256 + 90d salt rotation, pseudonymized id ↔ original id 변환표 owns). 본 branch 는 security/audit log 에 *pseudonymized 형태로만 기록* 계약만 owns. foundation §2 Q12 가 본 branch/[[raw/branch-notes/feature-security-operational-baseline]] 도 후보로 열거했으나 알고리즘 결정은 data-retention 보유로 확정.
- test 위임: structured log field 존재/계약 위반 테스트는 [[raw/branch-notes/feature-contract-verification-test-suite]] (`contract-verification:log-fields` / `log-mdc-keys`).
## Audit & Findings (ground-truth drift — 2026-06-13 /branch-spec)
> ca-tmpl `src/` 코드 대조로 발견한 노트↔구현 drift. **사용자 작성 결정 영역** 이므로 자동 rewrite 안 함 — 정합 권고만. 정합 시점에 본 § + 해당 결정/spec § 갱신.
| ID | Finding | 근거 (코드 grep) | 권고 |
|---|---|---|---|
| DRIFT-1 | ~~D10 `<springProfile>` human-readable encoder 미구현~~ **해소(2026-06-14)**: `logback-spring.xml``<springProfile name="local,dev">`=PatternLayout(`%maskedMsg` 포함) / `<springProfile name="!local &amp; !dev">`=LogstashEncoder(JSON) 분기. CONSOLE 단일 appender명으로 async wrap 공유 | `logback-spring.xml`(재작성) | 완료 |
| DRIFT-2 | ~~Redaction Layer 1 masking converter 미구현~~ **해소(2026-06-14)**: `LogMaskingPatterns`(정규식 SSOT: token/password/secret/api-key/authorization/bearer→`****`, capture-group replacement) → JSON은 `SecretMaskingJsonGeneratorDecorator`(logstash `MaskingJsonGeneratorDecorator``%replace`는 JSON encoder가 PatternLayout 우회하여 부적합, Claims To Verify L291 해소), pattern은 `SecretMaskingMessageConverter`(`%maskedMsg`). 단일 catalog로 profile 전환 시 마스킹 일관성 보장 | `LogMaskingPatterns.java` / `SecretMaskingJsonGeneratorDecorator.java` / `SecretMaskingMessageConverter.java` / `logback-spring.xml` + `LogMaskingPatternsTest`(token/password/bearer 마스킹 검증) | 완료 (Layer 2/3는 by-construction 보완 유지) |
| DRIFT-3 | ~~request log 가 `uri_template` 아닌 raw `getRequestURI()`~~ **해소(2026-06-14)**: `RequestLoggingFilter``HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE` 로 route template 추출, `path=``uri_template=` 필드명 변경, 신규 테스트(`logs_uri_template_not_raw_path_when_handler_mapping_attribute_set`) 추가 — actually-implemented, locally-verified | `RequestLoggingFilter.java` (수정됨) / `RequestLoggingFilterTest.java` (신규 테스트 추가) | 완료 |
| DRIFT-4 | ~~dependency log level=WARN(fail-open) + 필드/case 불일치 + 테스트 계약 충돌~~ **해소(2026-06-14, 사용자결정 Q1=어댑터종류 분리)**: `OutboundDependencyLogger` 필드 snake_case 정합(`dependency_name`/`dependency_type`/`operation`/`outcome`/`correlation_id`/`error`) + outcome 대문자(SUCCESS/FAILURE) + `dependency_type` 파라미터 추가 + 5 callers(kafka/outbox/slack/google-email/cache) 타입 전달. **레벨 정책**: optional fail-open=WARN 유지(유스케이스 성공 → 관측만, ERROR 미승격), core HTTP path(`OutboundHttpDependencyLogger`)=ERROR. duration_ms/error_code는 core HTTP path 전용으로 문서화 | `OutboundDependencyLogger.java` + 5 callers + 5 tests | 완료 |
| DRIFT-5 | ~~`log.appender.dropped.total` metric 미배선 + "drop oldest" 표현 부정확~~ **해소(2026-06-14)**: `MetricsAsyncAppender extends AsyncAppender``append()`에서 `isQueueBelowDiscardingThreshold() && isDiscardable()``Metrics.counter("log.appender.dropped.total","appender",name,"level",INFO/DEBUG)` 발행(`Metrics.globalRegistry` 경유 — Spring Boot가 앱 registry를 글로벌 composite에 추가). logback-spring.xml ASYNC_CONSOLE/ASYNC_FILE class 교체. 표현은 Sampling Policy 메모에서 정정 완료 | `MetricsAsyncAppender.java` + `MetricsAsyncAppenderTest`(discardingThreshold>queueSize 결정론 트릭) + `logback-spring.xml` | 완료 |
| DRIFT-6 | ~~`user_principal` raw 기록 (pseudonymization 미적용)~~ **해소(2026-06-14)**: `RequestLoggingFilter``UserPrincipalPseudonymizer` 생성자 주입, `MDC.put(USER_PRINCIPAL, pseudonymizer.pseudonymize(user.idpUserId()))` 로 교체(null 반환 시 미기록), 신규 테스트(`puts_pseudonymized_user_principal_on_mdc_not_raw_id`) 추가 — actually-implemented, locally-verified | `RequestLoggingFilter.java:81→95` (수정됨) / `RequestLoggingFilterTest.java` (신규 테스트 추가). 포트: `UserPrincipalPseudonymizer.java`(application-core); 구현체: `HmacUserPrincipalPseudonymizer.java`(adapter-identifier). **app-bootstrap 빈 배선 완료(2026-06-14)**: `PseudonymizationConfig`(@Bean `UserPrincipalPseudonymizer``HmacUserPrincipalPseudonymizer(salt)`) + `PrivacySettings`(@ConfigurationProperties `ca-skeleton.privacy.pseudonymization-salt`, warn-and-default) + `APP_PRIVACY_PSEUDONYMIZATION_SALT`(.env/env-keys.yaml/application.yml) | 완료 (전 경로 배선 + `PseudonymizationConfigTest`/`PrivacySettingsTest` green) |
| DRIFT-7 | ~~`logback-spring.xml` Janino `<if condition>` 토글이 logback 1.5.x status 경고 2종 유발: `IfNestedWithinSecondPhaseElementSC`(`<if>`-in-`<root>`) + `IfModelHandler`(`condition` 속성 deprecated, 2027 제거예정)~~ **해소(2026-06-14, 사용자 재요청)**: Janino `<if condition='property(K).equals(V)'>` 6곳 → 내장 `<condition class="ch.qos.logback.core.boolex.PropertyEqualityCondition"><key>K</key><value>V</value>` (조회 경로 `OptionHelper.propertyLookup(local,context)` 동일 → context-scoped `<springProperty>` 값 보존, 동작 무변경). `<root>``<if>` 3개는 logback 권고대로 `<root>` 를 top-level `<condition>+<if>` 로 감싸 un-nest(ASYNC×FILE 2×2). janino dep(build.gradle) 제거 | logback-core **1.5.34**(`dependencyInsight`), 소스 `condition=` 0건, `bootRun`(local): `\|-WARN/\|-ERROR` 0건 + 정상 기동(Tomcat:8080) + local PatternLayout 분기 정상 | 완료 (토글 origin=base-template f9ad280; 파일 최신 owner-touch d10a751=본 branch) |
## 테스트 계약
- dependency failure log에는 dependency name/type/duration/error code가 있어야 함.
- token/password/body가 log에 나오면 실패.
- 5xx와 dependency failure가 ERROR 이하로 기록되면 실패.
- requestId/traceId/correlationId 없는 request log는 실패.
- domain package가 logger를 직접 사용하면 실패.
> 위 계약은 `feature-contract-verification-test-suite` 가 실행 (`contract-verification:log-fields` / `log-mdc-keys`). 현 구현과의 gap 은 Audit & Findings(DRIFT-3/4/6) 참조 — 계약이 요구하는 `uri_template`/`dependency_type`/pseudonymized `user_principal` 이 코드에 아직 없으므로, 테스트 활성화 시 정합 작업이 선행되어야 함.
## 검증해야 할 주장
> 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
> 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| Logback `%replace(p){r, t}` 가 ca-tmpl logback.xml 의 token/password/auth header 정규식을 실제로 마스킹 | regex 기반 → false negative 가능 (Base64 token 등), encoder 적용 순서 검증 필요. **현재 Layer 1 미구현(DRIFT-2)** | unit test: log line 에 `token=abc123` / `Authorization: Bearer xxx` 주입 후 final encoder output 에서 `****` 확인 | `planned` |
| structured JSON encoder (`logstash-logback-encoder`) 와 `%replace` converter 의 적용 순서 — encoder 가 PatternLayout 우회 시 masking 누락 | Logback 공식 페이지에 encoder vs converter 순서 명시 없음. 실제 구현은 `LogstashEncoder`(PatternLayout 우회) → converter 적용점 별도 설계 필요 | integration test: JSON encoder 채택 후 masking pattern 통과 여부 확인 | `planned` |
| MDC 가 async/reactive thread 전환 시 자동 전파되지 않음 — TaskDecorator 또는 Micrometer Observation propagation 필요 | `LOG-LBK-C4` Does not prove: MDC 는 thread-local | async test: `@Async` 메서드 호출 후 MDC 의 `request_id` 가 유지되는지 확인 | `planned` |
| stdout JSON 의 ECS field naming 변환 (`traceId``trace.id`) 시 기존 alert/dashboard 영향 | `LOG-ECS-C2`/`C3` Does not prove: 자동 변환 보장 없음 | grep 으로 alert/dashboard config 에서 `traceId` 사용 위치 확인 후 일괄 변환 plan | `planned` |
| OTel log signal 채택 시 SeverityNumber 변환 (SLF4J INFO → OTel 912) 자동성 | `LOG-OTEL-C4` Does not prove: SLF4J ↔ OTel 1:1 매핑 보장 없음 | `OpenTelemetryAppender` 추가 후 LogRecord severity 검증 (별도 spike) | `needs-confirmation` |
| log sampling 10% / trace sampling 1% 비율이 운영 진단에 충분 — drop 된 INFO 가 incident 시 사후 부족 발생 여부 | 운영 가정, 실제 traffic + incident frequency 데이터 부재. **비율 sampling 자체 미구현(planned)** | prod 도입 후 1 분기 incident 회고 — log 부족으로 root cause 미해결 case 카운트 | `needs-confirmation` |
| audit log immutability 의 file system / object storage level 보장 (append-only 강제) | 본 branch 의 "append-only file appender" 결정은 application level 만 — OS / S3 versioning 별도 필요 | filesystem permission test + S3 object lock 정책 review | `planned` |
| local/dev 에서 human-readable pattern, staging/prod 에서 JSON encoder 가 `<springProfile>` 분기로 실제 적용되는지 (D10) | springProfile config 오류 시 silent fallback 가능 — 잘못된 profile 이 prod 에서 pattern 을 쓰거나 local 이 JSON 으로 떨어질 수 있음. **현재 분기 미구현(DRIFT-1)** | profile별 boot 후 첫 로그 라인 형식 assert (local=non-JSON pattern / prod=valid JSON) + local pattern 에서도 `%replace` 마스킹 동작 확인 | `planned` |
| request log 가 `uri_template`(low-cardinality) 로 기록되는지 — ~~현재 raw `getRequestURI()` (DRIFT-3)~~ **해소(2026-06-14)** | high-cardinality path 가 log/metric tag 폭주 유발; `BEST_MATCHING_PATTERN_ATTRIBUTE` 사용으로 정합 | `logs_uri_template_not_raw_path_when_handler_mapping_attribute_set` 테스트 locally-verified (BUILD SUCCESSFUL) | `actually-implemented` |
## 관심사 커버리지 (coverage-auditor 자동 생성)
> `/coverage` 가 생성하는 **생성물** — 손유지 금지. 기준: `rules/coverage-gate.md`. governing_docs: `observability-log-metric-trace-runbook`.
> 마지막 감사: 2026-06-13 → **Covered** (Blocking 0 / Should-fix 2 / Advisory 1). Should-fix 2건은 *타 branch*(data-retention·distributed-tracing)의 `## Coverage` 섹션 부재(UNLINKED_DELEGATION) — 본 branch 결정 범위 밖, follow-up 으로 이관.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| Structured JSON Logback 스키마 (필수 필드, ECS 호환 매핑) | covered-here | — | — | D1 (`LOG-LBK-C1/C3/C5`) + D6 (ECS 거부); `Log Type별 필수 필드` 표. `logback-spring.xml` JSON_CONSOLE + LogstashEncoder actually-implemented |
| Log type 분류 (request/dependency/security/audit/application) | covered-here | — | — | `Log Type별 필수 필드` 표; `mdc-keys.yaml` `owner_branch: feature-log-management-contract` 13 key (코드 ground-truth) |
| Level 정책 (INFO 이하 sampling, WARN/ERROR 100%) | covered-here | — | — | D8; `Sampling Policy (final)`; `logback-spring.xml` `discardingThreshold=20 neverBlock=false` actually-implemented |
| Sink 라우팅 (stdout JSON default, file local/dev only) | covered-here | — | — | D4 (`LOG-12F-C1/C2/C3`, `LOG-ECS-AWSLOGS-C1/C2`, K8s `LOG-K8S-C1~C4`); `FILE_ENABLED` toggle actually-implemented |
| Async overflow 정책 (queueSize/discardingThreshold/neverBlock) | covered-here | — | — | D8; Sampling Policy overflow 메모; `AsyncAppender queueSize=512` actually-implemented |
| Sampling 정책 (prod 10% INFO / WARN·ERROR 100% / profile 표) | covered-here | — | — | D5/D8; `Sampling Policy (final)`. 비율 TurboFilter 는 planned(DRIFT) 이나 *정책 결정* 은 covered |
| Masking/Redaction SSOT (Logback converter Layer 1 + Layer 2/3) | covered-here | — | — | D1; `Redaction Layer SSOT` §. Layer 1 planned(DRIFT-2)이나 governing doc 도 documented-only — SSOT 결정 자체는 covered |
| Alternatives evaluation (ECS vs OTel log signal vs 자체 schema) | covered-here | — | — | D6 (ECS 거부) + D7 (OTel 거부); `외부 근거 / 대안 조사` §. governing doc 이 본 branch 를 대안 검토 owner 로 명시 |
| Per-profile console encoder (D10) | covered-here | — | — | D10; Decisionized Work Items console format 행. 구현 planned(DRIFT-1)이나 *결정* 은 covered |
| MDC key naming SSOT | delegated | [[raw/branch-notes/feature-operational-error-observability-foundation]] | OK | D2 + §MDC Key Consumption. foundation §Coverage 가 "structured JSON Logback + masking/redaction + log sampling → 본 branch" 위임 명시 (양방향 링크 확인) |
| Retention 수치 + PII field allowlist | delegated | [[raw/branch-notes/feature-data-retention-privacy-contract]] | Should-fix | D9 + §Audit. **owner 노트에 `## Coverage` 섹션 부재 + 역링크 평문(UNLINKED_DELEGATION-1)** — follow-up: 해당 branch 에 Coverage 추가 + wikilink 정식화 |
| Trace sampling + traceparent 전파 | delegated | [[raw/branch-notes/feature-distributed-tracing-contract]] | Should-fix | D5 + §엣지(상호 확인된 짝). **owner 노트에 `## Coverage` 섹션 부재(UNLINKED_DELEGATION-2)** — follow-up |
| `user_principal` pseudonymization 알고리즘 | delegated | [[raw/branch-notes/feature-data-retention-privacy-contract]] | Advisory | 알고리즘 SSOT = data-retention 의 HMAC-SHA-256 + 90d salt rotation 결정(coverage-auditor 확인). 본 branch 는 *pseudonymized 형태로만 기록* 계약만 owns. 코드 raw `idpUserId()` 기록은 DRIFT-6 으로 capture — 정합 시 owner 알고리즘 적용 |
## 마주친 문제
- 아직 없음.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/container-stdout-logging-12factor-official]]
- [[raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official]]
- [[raw/official-docs/k8s-logging-architecture-kubernetes-official]]
- [[raw/official-docs/log-ecs-schema-elastic-official]]
- [[raw/official-docs/log-logback-mask-pattern-converter-official]]
- [[raw/official-docs/log-otel-log-data-model-spec]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: interviews:start -->
- [[raw/interviews/deterministic-logback-asyncappender-drop-metric-test-2026-06-14]]
<!-- GENERATED: interviews:end -->
<!-- GENERATED: errors:start -->
- [[raw/errors/webmvctest-component-filter-constructor-dep-breaks-slice-2026-06-14]]
<!-- GENERATED: errors:end -->
<!-- GENERATED: daily-notes:start -->
- [[raw/daily-notes/2026-06-14]]
<!-- GENERATED: daily-notes:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/logback-layer1-secret-masking-json-vs-pattern-2026-06-14]]
<!-- GENERATED: blog-topics:end -->
> Phase C2 실 구현 시작(2026-06-14): DRIFT-6 포트 인터페이스 생성. 이후 errors / interview prep 누적 시 추가.
### 오류 기록 (본 feature 작업 중 발생)
- **`@Component` Filter 에 생성자 의존성 추가 → `@WebMvcTest` 슬라이스 컨텍스트 로드 실패** (`OperationalContractRuntimeTest`). `addFilters=false` 여도 `@WebMvcTest``Filter` 빈을 *인스턴스화* 하므로 `RequestLoggingFilter(UserPrincipalPseudonymizer)`가 빈 부재로 `NoSuchBeanDefinitionException`. 광역 스캔(`@WebMvcTest(CaSkeletonApplication)`)만 영향, 패키지-국한 슬라이스(sample-portfolio)는 무영향. 해결=`@Import(PseudonymizationConfig.class)`. → [[raw/errors/webmvctest-component-filter-constructor-dep-breaks-slice-2026-06-14]]
- **Pre-existing(본 작업 무관) ArchUnit 실패**: `outbound_adapter_method_returns_only_domain_or_primitives` on `OutboundHttpSettings.circuitBreaker()/retry()`(@ConfigurationProperties record accessor가 nested config record 반환). commit d702572 도입, stash한 clean tree에서도 실패로 확인. B7 규칙이 @ConfigurationProperties accessor를 false-positive로 잡는 rule-precision 이슈 — feature-boundary-validation-mapping-contract / outbound-http-client-baseline 소유. 본 브랜치 scope 외, 미수정.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- HMAC-SHA-256 을 pseudonymizer 로 선택한 이유 / 단방향성 증명 방법 / salt rotation 90일 근거 — `feature-data-retention-privacy-contract` SSOT consume.
- `javax.crypto.Mac` 이 thread-safe 하지 않은 이유 + singleton bean 에서 thread safety 확보 방법 (per-call 인스턴스 생성 vs ThreadLocal vs instance pool).
- `HexFormat.of().formatHex(byte[])` — Java 17 도입 API, 기존 `String.format("%02x")` 루프 대비 장점.
- 구조화 JSON 로그 마스킹: `%replace``LogstashEncoder`(JSON)에 안 걸리는 이유 + `MaskingJsonGeneratorDecorator` 대안. AsyncAppender 드롭 메트릭 결정론 테스트. → [[raw/interviews/deterministic-logback-asyncappender-drop-metric-test-2026-06-14]]
### Blog topics
- "Spring-free 모듈에서 crypto adapter 구현하기 — `adapter-identifier` 설계 결정" (HMAC-SHA-256 구현, `Mac` thread safety, `implementation` vs `api` Gradle 선택, forbidden Spring 어노테이션)
- "Logback Layer 1 secret masking: `%replace`로는 JSON을 못 가린다 — 단일 정규식 SSOT로 encoder/pattern 양 경로 일관 마스킹" → [[raw/blog-topics/logback-layer1-secret-masking-json-vs-pattern-2026-06-14]]
## 관련 일일 노트
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- [[raw/daily-notes/2026-06-14]] (DRIFT-6 UserPrincipalPseudonymizer 포트 인터페이스 생성 + HmacUserPrincipalPseudonymizer 구현체 생성 + DRIFT-3/DRIFT-6 RequestLoggingFilter 연결 — uri_template 로그 + pseudonymized user_principal MDC)
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):