431 lines
49 KiB
Markdown
431 lines
49 KiB
Markdown
---
|
||
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 & !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 9–12) 자동성 | `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):
|