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

49 KiB
Raw Permalink Blame History


branch: feature-log-management-contract

Layer: raw/branch-notes/ — structured log schema와 로그 금지 정책을 정의합니다.

부모 (필수)

raw/project-notes/ca-skeleton-operational-contract

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: log field·masking contract와 verification test가 통과한다

상속한 프로젝트 결정

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

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

없음.

목표

운영자는 exception class보다 어떤 operation, dependency, retryable 여부, trace/correlation 정보가 필요한지 봅니다. 이 branch는 skeleton의 log contract를 확정합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • 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)

채택 결정 + 뒷받침

검토 대안 + source

비교 핵심 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 Claimsraw/<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_principalmdc-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 필요.
  • 다른 계약 의존:

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): RequestLoggingFilterHandlerMapping.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 AsyncAppenderappend()에서 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): RequestLoggingFilterUserPrincipalPseudonymizer 생성자 주입, 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 UserPrincipalPseudonymizerHmacUserPrincipalPseudonymizer(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 변환 (traceIdtrace.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 알고리즘 적용

마주친 문제

  • 아직 없음.

묶음

Phase C2 실 구현 시작(2026-06-14): DRIFT-6 포트 인터페이스 생성. 이후 errors / interview prep 누적 시 추가.

오류 기록 (본 feature 작업 중 발생)

  • @Component Filter 에 생성자 의존성 추가 → @WebMvcTest 슬라이스 컨텍스트 로드 실패 (OperationalContractRuntimeTest). addFilters=false 여도 @WebMvcTestFilter 빈을 인스턴스화 하므로 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 로그 마스킹: %replaceLogstashEncoder(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):