Files
llm-wiki/raw/branch-notes/feature-metrics-alerting-contract.md
T

48 KiB

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-metrics-alerting-contract branch-note raw feature-metrics-alerting-contract
ca-skeleton
wiki/projects/ca-tmpl/observability-log-metric-trace-runbook
branch
ca-skeleton
metrics
alerting
observability
2026-05-22 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-019 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-019
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1
1 3b83f7a53dc82997a9f9d3ff12f2106e16782bce65d5f95f8516a46532b0b2ee

branch: feature-metrics-alerting-contract

Layer: raw/branch-notes/ — metrics와 alerting 기준을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다. governing 문서는 wiki/projects/ca-tmpl/observability-log-metric-trace-runbook (§Metric).

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: metric key·cardinality·alert contract test가 통과한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1 foundation이 envelope schema와 error.category enum의 단일 owner다 Work Item 완료 조건에 적용 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

목표

로그만으로 운영 감시는 부족합니다. skeleton은 HTTP, dependency, DB pool, JVM, retry/circuit breaker의 기본 metric과 P1/P2/P3 alert severity를 가져야 합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • HTTP latency/error rate metric.
  • dependency latency/error rate metric.
  • DB pool metric.
  • JVM/process metric.
  • retry/circuit breaker metric.
  • alert severity P1/P2/P3 기준.
  • metric naming/tag 기준.

제외 범위

  • Grafana dashboard 구현.
  • Prometheus/CloudWatch 특정 vendor 설정.
  • SLO/SLA 정식 수립.

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/metric-micrometer-naming-convention-official.md Micrometer dot
raw/company-tech-blogs/metric-toss-payments-alert-severity-techblog.md 국내 fintech의 P1/P2/P3 운영 사례
raw/official-docs/metric-google-sre-slo-burn-rate.md threshold alert가 baseline이고 burn-rate는 후속 도입이라는 ca-tmpl 결정이 SRE Workbook 전형적 진화 경로와 정합함을 확인
raw/official-docs/metric-otel-metrics-data-model-spec.md naming 일부 다름(`http
raw/official-docs/resilience4j-micrometer-module Resilience4j Micrometer 모듈 — resilience4j.circuitbreaker.calls/state/resilience4j.retry.calls/bulkhead.queue.depth/ratelimiter.available.permissions metric 명 + kind/name tag 의 1차 근거 (D4 retry/CB metric default consume 직접 증명)
raw/official-docs/metric-micrometer-high-cardinality-tags-detector D8 — unbounded tag(userID/requestID/traceID 등)가 millions of time series + excessive memory consumption 야기함을 Micrometer 공식 문서가 명시. high-cardinality 금지 tag 목록의 직접 근거 (MM-HCARD-C1, MM-HCARD-C2)
raw/official-docs/metric-prometheus-label-cardinality-best-practices D8 — Prometheus 공식 — "every unique combination of key-value label pairs represents a new time series" + user IDs / email / unbounded set label 금지 직접 경고 (PROM-CARD-C1, PROM-CARD-C2)
raw/official-docs/metric-micrometer-histogram-percentile-concepts D9 — latency timer 의 percentile/histogram 게시 전략 (publishPercentiles vs publishPercentileHistogram vs serviceLevelObjectives). client-side percentiles 가 dimension 간 집계 불가하다는 공식 caveat (MM-HIST-C2, MM-HIST-C4).
raw/official-docs/metric-google-sre-workbook-on-call D10 — alert(page)가 monitoring console(dashboard) 링크를 포함해야 하고, 각 alert 에 playbook/runbook entry 가 있어야 한다는 Google SRE 공식 근거 (SRE-ONCALL-C1, SRE-ONCALL-C2, SRE-ONCALL-C4)
raw/official-docs/metric-google-ewaschuk-philosophy-on-alerting D10 — "각 alert/alert family 마다 playbook(runbook) entry" 원칙 + "page 는 actionable" + 4원칙(urgent/important/actionable/real) 의 직접 근거 (SRE-PHIL-C1, SRE-PHIL-C2, SRE-PHIL-C3)
raw/official-docs/metric-prometheus-histograms-vs-summaries-practices D9 — Summary quantile 을 인스턴스 간 avg() 로 집계하면 통계적으로 무의미하다는 Prometheus 공식 경고 (PROM-HIST-C1, PROM-HIST-C2). classic histogram 올바른 집계 구문 (PROM-HIST-C3).

외부 근거 / 대안 조사 (2026-05-22 — Group G-A: Metrics alerting)

채택 결정 + 뒷받침

검토 대안 + source

비교 핵심 1줄

Micrometer + Prometheus는 Spring Boot 3 default + JVM 생태계 표준으로 도입 비용 최저, OTel metrics는 cross-language 통일, SLO burn-rate는 SLO 수립 후 단계.

TODO

TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Metric / Alert Defaults" / "Cardinality Bounds" / "Histogram Buckets / Percentile" / "P1/P2/P3 정량 기준" / "Retry / CircuitBreaker / DB Pool Minimum Metric Set" 참조. HTTP/dependency/DB pool/JVM/retry-CB/alert severity/naming-tag 모두 표 또는 결정 라인으로 반영됨. 잔존 TODO 없음.

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 승급 위치

진행 중 메모

결정 사항 (decisions)

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. Decision ID 는 이 branch-note 안에서 안정적으로 유지한다. Supporting Claimsraw/<category>/<slug>.md#<CLAIM-ID> 형식으로 연결한다.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 metrics/alerting 을 log contract 와 별도 branch 로 분리 N/A (조직 운영 정책) UNSUPPORTED_DECISION (조직 / branch 분할은 내부 운영 정책 — 외부 raw 근거 없음) N/A branch 분할 자체는 외부 표준 인용 대상 아님. 운영 편의
D2 metric naming = Micrometer dot.case default + unit suffix (.seconds/.bytes/.total) 강제 JVM/Micrometer 스택일 때 이 결정. cross-language 통일 필요 시 D6 대안(OTel) raw/official-docs/metric-micrometer-naming-convention-official.md#MM-NAME-C1, raw/official-docs/metric-micrometer-naming-convention-official.md#MM-NAME-C2, raw/official-docs/metric-micrometer-naming-convention-official.md#MM-NAME-C3 official-vendor-doc (Micrometer reference — lowercase dot 컨벤션 + 시스템 별 자동 변환 + http.server.requests 예시) MM-NAME-C4 (suffix .count/.total 자동 부착) 및 MM-NAME-C5 (base unit handling) 는 본 페이지 발췌에 명시 없음 → needs-confirmation (concepts/timers 별도 fetch 필요). unit suffix 강제 정책의 표준 출처 미확보
D3 alert threshold = SLO/error budget 또는 documented operational default 에 연결 (임의 수치 금지) SLO 수립 후엔 burn-rate(D5)로 전환, 미수립 단계엔 documented default raw/official-docs/metric-google-sre-slo-burn-rate.md#SRE-BURN-C1, raw/official-docs/metric-google-sre-slo-burn-rate.md#SRE-BURN-C6 official-vendor-doc (Google SRE Workbook — SLO 를 actionable alert 으로 + error budget 정의) SRE-BURN-C1 Usage Boundary: SLO 미수립 서비스에 적용 가능하다는 뜻 아님. ca-tmpl 의 "잠정 SLO p99=1s" 는 정식 SLO 가 아님
D4 retry/CB metric = Resilience4j default consume retry/CB 라이브러리가 Resilience4j 일 때 (owner: outbound branch) raw/official-docs/resilience4j-micrometer-module.md#R4J-MICROMETER-C1 (Micrometer 모듈이 InfluxDB/Prometheus 등 monitoring system 지원), #R4J-MICROMETER-C2 (resilience4j.circuitbreaker.calls + kind (successful/failed/ignored) + name tag), #R4J-MICROMETER-C3 (resilience4j.circuitbreaker.state Gauge + 5개 state: closed/open/half_open/forced_open/disabled), #R4J-MICROMETER-C4 (TaggedRetryMetrics.ofRetryRegistry(...).bindTo(meterRegistry) 패턴) official-vendor-doc (Resilience4j 공식 docs verbatim — 2026-05-27 확인) Spring Boot starter (resilience4j-spring-boot3) 자동 bind 동작은 cited raw 범위 밖 — R4J-MICROMETER-C2 Usage Boundary 명시 ("Spring Boot starter 가 자동으로 bind 한다는 뜻은 본 인용에서는 명시 안 됨"). state Gauge value 가 boolean 인지 enum index 인지 미명시 — 실측 필요. histogram/percentile default 노출 여부도 cited raw 범위 밖. enum drift: registry resilience4j.circuitbreaker.state 는 6 state(CLOSED/OPEN/HALF_OPEN/DISABLED/FORCED_OPEN/METRICS_ONLY)를 선언 — R4J-MICROMETER-C3 인용(5 state)보다 METRICS_ONLY 1개 많음. owner feature-outbound-http-client-baseline 와 enum 정합을 코딩 전 확인
D5 burn-rate 기반 alert 는 추후 도입 (현재 단순 threshold) SLO 정식 수립 후 이 결정 폐기 → burn-rate 채택 raw/official-docs/metric-google-sre-slo-burn-rate.md#SRE-BURN-C2, raw/official-docs/metric-google-sre-slo-burn-rate.md#SRE-BURN-C3, raw/official-docs/metric-google-sre-slo-burn-rate.md#SRE-BURN-C4, raw/official-docs/metric-google-sre-slo-burn-rate.md#SRE-BURN-C5 official-vendor-doc (paging 시작값 2%/1h + 5%/6h, multi-window 1/12 ratio, burn-rate powerful) SRE-BURN-C5 Usage Boundary: threshold alert 가 항상 inferior 라는 결론 아님 — SLO 미수립 단계에서는 threshold 가 가능한 fallback. ca-tmpl 의 현재 단계와 정합
D6 OpenTelemetry metrics 직접 채택 거부 (Micrometer + Prometheus 유지) cross-language 신호 통일이 필수가 되면 재검토 (Micrometer OTLP bridge swap) raw/official-docs/metric-otel-metrics-data-model-spec.md#OTEL-MET-C1, raw/official-docs/metric-otel-metrics-data-model-spec.md#OTEL-MET-C2 official-standard (OTel data model 의 Prometheus Remote Write 변환 보장 — swap 가능성) OTEL-MET-C7 (HTTP semantic conventions 의 required attributes) 는 본 페이지에 명시 없음 → needs-confirmation. ca-tmpl 의 Micrometer naming (uri_template) 과 OTel semconv (http.route) 정합 별도 검증
D7 P1/P2/P3 정량 기준 (잠정 SLO 기반): P1=>5%/5분, P2=>1%/10분, P3=>0.1%/1시간 정식 SLO 합의 시 burn-rate(D5) 기반으로 재산정 UNSUPPORTED_DECISION (raw/company-tech-blogs/metric-toss-payments-alert-severity-techblog.md#TOSS-ALERT-C3 는 unverified — 출처 검증 실패. 본 raw 의 TOSS-ALERT-C3/C4/C5/C6/C7 모두 needs-confirmation. company-tech-blog 는 official best practice 가 아님) company-case-study (TOSS-ALERT-C1 verified only — 로깅 inputs) 정량 threshold 값은 ca-tmpl 잠정 SLO 의 운영 가정. 외부 공식 표준 없음. toss 사례를 official best practice 로 표현 금지
D8 cardinality bounds: user_id/request_id/raw_url 등 high-cardinality tag 금지 + bounded whitelist (status_code≤7 / uri_template≤200 / dependency_name≤50) tag 값이 unbounded(사용자 입력 유래) 면 금지·정규화. trace_id 등 개별 식별자가 필요하면 exemplar/trace 로(D 의존: tracing) raw/official-docs/metric-prometheus-label-cardinality-best-practices.md#PROM-CARD-C1, raw/official-docs/metric-prometheus-label-cardinality-best-practices.md#PROM-CARD-C2, raw/official-docs/metric-micrometer-high-cardinality-tags-detector.md#MM-HCARD-C1, raw/official-docs/metric-micrometer-high-cardinality-tags-detector.md#MM-HCARD-C2 official-vendor-doc (Prometheus 공식 — high-cardinality label 이 time series 폭증 야기 직접 경고; user IDs / email / unbounded set 금지 명시 + Micrometer 공식 userID/requestID/traceID 명시) PROM-CARD-C2 는 user_id/email/unbounded set 을 예시로 열거 — request_id/raw_url/ip_address 금지는 이 원칙에서 추론한 적용. UNSUPPORTED_IMPL_DECISION: 정량 상한값(≤200/≤50 등)은 공식 spec 없는 운영 가정
D9 latency timer = SLO-driven 분포 게시. registry(metrics.yaml)는 timer 행마다 percentiles: [0.5,0.9,0.95,0.99](client-side) + histogram_buckets: slo_driven(aggregable) 둘 다 선언 Prometheus + 다중 인스턴스 → 집계는 histogram 버킷. 단일 인스턴스 즉시 가시성만 필요 → client-side percentile 로 충분 raw/official-docs/metric-micrometer-histogram-percentile-concepts.md#MM-HIST-C2 (Prometheus 대상 시 histogram 게시 공식 권장 — 차원 간 집계 가능), #MM-HIST-C4 (client-side percentile 은 redundant + non-aggregable across dimensions), raw/official-docs/metric-prometheus-histograms-vs-summaries-practices.md#PROM-HIST-C1 (quantile 평균은 통계적으로 무의미), #PROM-HIST-C3 (histogram_quantile() 가 올바른 집계 구문) official-vendor-doc (Micrometer) + official-standard (Prometheus) 설계 위험: client-side publishPercentiles 값은 인스턴스 간 avg()/sum() 불가(PROM-HIST-C2 // BAD!). 다중 인스턴스 cross-instance p99 은 slo_driven 버킷 + histogram_quantile() 가 source of truth. publishPercentileHistogram 기본 ~73 버킷/dim → cardinality 부담(min/maxExpectedValue 튜닝). UNSUPPORTED_IMPL_DECISION: SLO 경계값(100ms/500ms/1s)은 잠정 SLO 역산 — 공식 근거 없음
D10 alert payload = runbook + dashboard(monitoring console) 링크 [official-supported] + log query 링크 [operational default] runbook/dashboard 가 존재하면 링크 강제. 미작성 단계엔 placeholder 허용 runbook+dashboard: raw/official-docs/metric-google-sre-workbook-on-call.md#SRE-ONCALL-C1 ("Ensure pages link to relevant monitoring consoles"), #SRE-ONCALL-C4 ("Each alert should have a corresponding playbook entry"), raw/official-docs/metric-google-ewaschuk-philosophy-on-alerting.md#SRE-PHIL-C1 (alert/family 마다 playbook entry), #SRE-PHIL-C2 ("Every page should be actionable") · log query: UNSUPPORTED_IMPL_DECISION (SRE 문헌이 log query URL 까지는 직접 명문화 안 함 — ca-tmpl 운영 default) official-vendor-doc (Google SRE — runbook+dashboard) / operational-default (log query) Ewaschuk 문서는 playbook entry 의 필요성을 말함 — alert annotation 에 URL embed 를 직접 명문화하진 않음(SRE-ONCALL-C1 의 "pages link to consoles" 가 dashboard 링크를 직접 지지). log query URL 은 vendor-specific(CloudWatch/Kibana/Loki) → 환경 이전 시 깨질 수 있음. toss(TOSS-ALERT-C5) 는 여전히 unverified — official 표현 금지

구현 가이드

결정 (Decisions) 이 "무엇" 이라면, 본 §는 "어디에 어떻게" 의 사전 명세. 상세 표(아래 §Metric/Alert Defaults · §Cardinality Bounds · §Histogram Buckets/Percentile · §P1/P2/P3 · §Retry/CB/DB Pool)가 값의 SSOT 이며, 본 §는 그 표를 코드·registry anchor 에 연결한다. 계약 값의 최종 SSOT = ca-tmpl/docs/registries/metrics.yaml (owner_branch).

1. Metric registry SSOT 와 owner 분할

Trace: D2(naming)·D8(cardinality)·D9(histogram) + In-scope 모든 metric. Supporting anchor: ca-tmpl/docs/registries/metrics.yamlowner_branch: 행 (계약 값의 SSOT — invent 금지).

본 branch 가 소유(owner_branch: feature-metrics-alerting-contract)하는 registry 행 — 정의·tag·alert threshold 가 본 branch 결정:

metric type tags (cardinality 상한) alert
http.server.requests timer (seconds) method≤8 / status≤7 / uri_template≤200 P1 err>5%/5m·>10%/1m, P2 >1%/10m, P3 >0.1%/1h
http.server.requests.latency timer method≤8 / uri_template≤200 P1 p99>5s/5m, P2 p99>1s/10m, P3 p99>500ms/30m
dependency.client.requests timer dependency_name≤50 / dependency_type≤10 / outcome≤5 P1 required dep down 2m, P2 optional degraded 5m, P3 spike 10x
db.query.duration timer operation≤20 / outcome≤3 P2 p99>1s/10m
jvm.memory.used gauge (bytes) area≤2 / id≤10 P2 heap/max>0.85/10m
jvm.gc.pause timer action≤10 / cause≤10 P2 p99>500ms/10m
jvm.threads.live gauge (total) P3 >2x baseline/30m
process.uptime gauge (seconds) P1 uptime reset <60s (crash loop)

다른 branch 가 소유하는 행을 consume(본 branch 는 alert severity/cardinality 계약만 정합, 정의는 owner):

consumed metric(s) owner branch 본 branch reference
resilience4j.retry.calls / circuitbreaker.state / circuitbreaker.calls raw/branch-notes/feature-outbound-http-client-baseline D4, §Retry/CB/DB Pool
hikaricp.connections.acquire / usage / active feature-persistence-failure-baseline §Retry/CB/DB Pool
executor.* / job.* feature-background-job-async-contract (alert severity 정합만)
lock.* feature-distributed-lock-contract (cardinality 정합만)
outbox.* feature-domain-event-outbox-contract (cardinality 정합만)
cache.* feature-cache-consistency-contract (cardinality 정합만)
log.appender.dropped.total feature-log-management-contract §진행 중 메모 (log↔metric 정합)
tracing.sampling.rate feature-distributed-tracing-contract §엣지 (exemplar 위임)

2. 강제 메커니즘 (enforcement) — 현재 등급

Trace: D8(cardinality)·D2(naming) + §테스트 계약. Supporting anchor: src/ grep (2026-06-14).

  • registry 모든 행은 required_test: contract-verification:metrics-cardinality 선언 → 계약 위반 시 실패해야 하는 테스트.
  • 실측(2026-06-14 src/ grep): shared-contract/src/main/java/dev/caskeleton/shared/metrics/ 패키지는 비어 있음. cardinality/naming 강제 클래스 + contract-verification:metrics-cardinality 테스트 = planned(미구현). 본 branch 소유 HTTP/dependency/JVM timer 계측 코드(MeterRegistryCustomizer/Timer.builder config)도 미작성 = documented-only.
  • 현재 등급 요약: registry/계약 = documented-only; 코드 계측 + 강제 테스트 = planned. (sibling 의 BackgroundJobMetrics/OutboxMetrics/MeteredDistributedLockPort/OutboundHttpResilienceConfigactually-implemented — 각자 owner 범위, 본 branch 자기 보고로 FACT 화 금지.)
  • 실측(2026-06-15 구현 Task 1): shared-contract 모듈에 4개 pure contract type 추가 — actually-implemented + locally-verified:
    • AlertSeverity (enum, D7) — P1/P2/P3, key(), fromKey(String) case-insensitive. 11 tests PASS.
    • MetricNaming (final class, D2/D3) — ALLOWED_UNITS, isValidName(), isAllowedUnit(), toPrometheusName(). 27 tests PASS.
    • ForbiddenMetricTags (final class, D8) — FORBIDDEN, isForbidden(), firstForbidden(). 16 tests PASS.
    • CardinalityBounds (final class, §Cardinality Bounds) — named int constants + limitFor(). 16 tests PASS.
    • 모두 Java stdlib only (import 검증 완료). ./gradlew :shared-contract:test BUILD SUCCESSFUL.
  • 실측(2026-06-15 구현 Task 2): app-bootstrap 모듈에 runtime enforcement + contract test 추가 — actually-implemented + locally-verified:
    • MetricsCardinalityMeterFilter (implements MeterFilter, D8 runtime deny-list) — accept() returns DENY for any forbidden tag key in ForbiddenMetricTags.FORBIDDEN. 11 tests PASS.
    • MetricsDistributionMeterFilter (implements MeterFilter, D9 SLO-driven histogram) — configure() applies percentilesHistogram(true) + percentiles(0.5,0.9,0.95,0.99) + SLO boundaries (100ms/500ms/1s/5s) + min/maxExpected for 5 owned timers (http.server.requests, http.server.requests.latency, dependency.client.requests, db.query.duration, jvm.gc.pause); passes through unchanged for non-owned meters. 23 tests PASS. (Fix 2026-06-15: dependency.client.requests was initially missing from SLO_DRIVEN_TIMERS despite being an owned slo_driven timer per metrics.yaml:84,89 — spec reviewer Req #10 PARTIAL finding. Added in surgical correction with TDD red→green proof.)
    • MetricsContractConfig (@Configuration) — ObjectProvider<MeterRegistry> + @PostConstruct installFilters(); public static install(MeterRegistry) for testability; no-op when registry absent. 5 tests PASS.
    • MetricsAlertingContractTest — 15 contract tests (global D2/D8/D9/cardinality/alert-key checks + row-specific #1/#2/#3/#4 + MeterFilter behaviour + new registry↔filter coverage drift guard); all 15 PASS locally (metrics.yaml present). Assumptions.assumeTrue(metricsRoot != null, ...) guard in place — skips (not fails) when docs/registries/metrics.yaml absent on CI. Unused import java.util.Collection; removed.
    • ./gradlew :app-bootstrap:test BUILD SUCCESSFUL (full suite). ./gradlew verifyCleanArchitectureDependencies BUILD SUCCESSFUL.
    • 설치 방식: MeterFilter.@Bean 방식 아님 — registry.config().meterFilter(...) 직접 (OutboundHttpResilienceConfig I8 패턴 미러). No new Gradle dependencies added.
  • UNSUPPORTED_IMPL_DECISION: 강제 메커니즘 형태 — MeterFilter deny-list(MM-HCARD-C4) vs registry-vs-actuator diff 스모크 vs runtime HighCardinalityTagsDetector(MM-HCARD-C5 는 Observation API 만 권고) — 는 근거 raw 가 원칙만 권고하고 메커니즘은 비권고 → 구현자 trade-off. 권고: MeterFilter deny-list + registry↔/actuator/prometheus diff 스모크 병행.

3. unit suffix 변환

Trace: D2 + MM-NAME-C1~C3.

  • registry naming = Micrometer dot.case. Prometheus exposition 시 ._, unit suffix(seconds/bytes/total) 자동 변환.
  • UNSUPPORTED_IMPL_DECISION: Spring Boot 3 가 http.server.requests 에 자동 부착하는 정확한 Prometheus suffix(_seconds_bucket/_count/_sum)는 cited raw 미명시 → §Claims To Verify 의 actuator 확인 항목으로 위임.

Metric / Alert Defaults

item default forbidden
HTTP metric http.server.requests with method/status/uri-template raw URL or user id tag
dependency metric dependency.client.requests with dependency.name/type/outcome endpoint with secret tag
retry metric Resilience4j retry/circuit metric retry without metric
alert severity P1, P2, P3 severity missing
threshold source SLO/default table unexplained magic number

Cardinality Bounds

tag 상한 (per metric)
status_code 7 (1xx-5xx + ok/other)
uri_template 200
dependency_name 50
error_code 100 — error registry(ca-tmpl/docs/registries/error-codes.yaml)의 row 상한과 정합. registry 상한 변경 시 본 표 동시 업데이트.
tenant_id 1000 (활성 시) — ULID 원본을 직접 사용하지 않음. metric label로는 (a) bounded mapping table id (tenant 등록 시 ascending integer 부여) 또는 (b) tenant cohort bucket(예: hash mod 100) 사용. 1001번째 tenant 등장 시 cardinality 정책: 새 tenant는 bucket으로 자동 fold.
outcome (resilience4j) 5 (SUCCESS/FAILURE/CIRCUIT_OPEN/TIMEOUT/REJECTED)

high-cardinality 금지 tag: user_id, request_id, raw_url, raw_query, raw_header_value, ip_address. (근거: PROM-CARD-C1/C2 user IDs·email·unbounded set 금지 + MM-HCARD-C1/C2 userID/requestID/traceID → millions of time series.)

Histogram Buckets / Percentile

Trace: D9. registry SSOT = ca-tmpl/docs/registries/metrics.yaml (timer 행의 percentiles + histogram_buckets: slo_driven).

  • HTTP latency / DB query / dependency call (registry timer 행 공통):
    • aggregable 소스 (권장 source of truth): publishPercentileHistogram() + serviceLevelObjectives(...) → registry histogram_buckets: slo_driven. Prometheus histogram_quantile(0.95, sum by (le)(rate(..._bucket[5m]))) 로 인스턴스 간 집계 (MM-HIST-C2, PROM-HIST-C3).
    • client-side 편의값: publishPercentiles(0.5, 0.9, 0.95, 0.99) → registry percentiles: [...]. 단일 인스턴스 즉시 가시성용. 인스턴스 간 집계 금지 (MM-HIST-C4, PROM-HIST-C1/C2 // BAD!).
  • bucket = SLO-driven. 명시적 SLO 미수립 시 잠정 SLO p99 = 1s 사용. (SLO 경계값은 UNSUPPORTED_IMPL_DECISION — 잠정 SLO 역산.)
  • publishPercentileHistogram 기본 ~73 버킷/dim → minimumExpectedValue/maximumExpectedValue 로 범위 제한해 cardinality 관리.

P1/P2/P3 정량 기준 (잠정 SLO 기반)

severity error rate latency p99 dependency lag scope
P1 >5% 5분 지속 또는 >10% 1분 p99 > 5s 5분 required dep unavailable >2분 release-blocking incident
P2 >1% 10분 지속 p99 > 1s 10분 optional dep degraded > 5분 on-call 즉시 대응
P3 >0.1% 1시간 지속 p99 > 500ms 30분 spike alert (10x baseline) business hours 대응

burn-rate 기반 alert는 추후 도입(현재는 단순 threshold). 위 수치는 D7 = UNSUPPORTED_DECISION (잠정 SLO 운영 가정 — 외부 공식 표준 없음).

Retry / CircuitBreaker / DB Pool Minimum Metric Set

  • retry/CB minimum: resilience4j.retry.calls{outcome}, resilience4j.circuitbreaker.state, resilience4j.circuitbreaker.calls{outcome}. (owner: raw/branch-notes/feature-outbound-http-client-baseline, D4 consume.)
  • DB pool exhaustion 감지 metric: hikaricp.connections.acquire{outcome="timeout"} p99 > 100ms. (owner: feature-persistence-failure-baseline.)

엣지·실패·의존

R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존.

  • 실패·엣지 경로:
    • high-cardinality leak: uri_template 미정규화 시 404/raw path 가 series 폭증 (MM-HCARD-C2, PROM-CARD-C1). 기대 동작: MeterFilter deny + uri 정규화 → bounded(≤200). 미정규화 metric 은 metrics-cardinality 테스트 실패.
    • cross-instance percentile 오집계: client-side publishPercentiles 를 인스턴스 간 avg() (PROM-HIST-C2 // BAD!) → 통계적 무의미값. 기대 동작: 집계는 slo_driven 히스토그램 버킷 + histogram_quantile() 만.
    • registry drift: metrics.yaml 행 ↔ 실제 노출 metric(tag 추가/이름 변경) 불일치. 기대 동작: registry↔/actuator/prometheus diff 스모크 실패.
    • threshold 누락/임의수치: SLO/default table 근거 없는 magic number → §테스트 계약 위반.
    • error_code tag 상한 초과: error-codes.yaml row > 100 이면 cardinality cap 초과 → §Cardinality Bounds 표 + registry 동시 업데이트 필요.
    • tenant_id 1001번째: bucket 자동 fold(§Cardinality Bounds). ULID 원본 직접 label 금지.
  • 다른 계약 의존:

테스트 계약

  • HTTP request metric에 method/status/uri template tag가 없으면 실패.
  • dependency metric에 dependency.name/type이 없으면 실패.
  • DB pool exhaustion을 감지할 metric 기준이 없으면 실패.
  • alert severity가 없는 dependency outage 기준은 실패.
  • high-cardinality tag가 metric에 들어가면 실패.
  • alert threshold의 근거가 SLO/default table에 없으면 실패.

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

Claim Why uncertain How to verify Status
Spring Boot 3 default meter (http.server.requests) 가 Micrometer 자동 변환으로 Prometheus 에서 http_server_requests_seconds_* 로 노출 MM-NAME-C3 의 timer 예시는 http_server_requests_duration_seconds 표기 — Spring Boot 3 + Micrometer 버전에 따라 suffix 차이 존재 actuator /actuator/prometheus 응답에서 실제 metric name 확인 planned
ca-tmpl 의 uri_template tag (Micrometer naming) 과 OTel semconv http.route 의 정합성 OTEL-MET-C7 미명시 (본 페이지 범위 밖) — semconv 별도 페이지 확인 필요 OTel Java instrumentation + Spring MVC 통합 시 http.route attribute value 확인 needs-confirmation
P1 threshold "(>5% 5분 또는 >10% 1분)" 가 SLO 99.9% 기준 burn rate 으로 환산 시 약 50x 정당성 SRE-BURN-C2 의 2%/1h + 5%/6h reasonable 시작값만 직접 지지 — 50x 환산은 별도 계산 SLO 99.9% 가정 + 실제 traffic 으로 burn rate 산출 + multi-window 표 비교 planned
HikariCP DB pool exhaustion 감지 metric (hikaricp.connections.acquire{outcome="timeout"} p99 > 100ms) 의 정확한 metric name HikariCP / Spring Boot 3 default meter 명세 본 branch 인용 자료에 없음 actuator /actuator/prometheus 에서 HikariCP metric name 확인 planned
Resilience4j default metric 이름 (resilience4j.retry.calls{outcome}, resilience4j.circuitbreaker.state) 의 verbatim 본 branch 인용 자료에 Resilience4j docs 없음 Resilience4j Micrometer integration docs 별도 raw 등록 + actuator 확인 needs-confirmation
client-side publishPercentiles + publishPercentileHistogram 동시 선언 시 Micrometer 가 둘 다 노출하는지 (혼합 모드 동작) MM-HIST-C4 는 "redundant" 라고만 명시 — 실제 노출 여부 미확인 actuator /actuator/prometheus 에서 _bucket + quantile gauge 동시 존재 확인 planned
toss 의 P1/P2/P3 정의 (결제 차단/일부 가맹점/내부 지표) 가 실제 toss 공식 정책 TOSS-ALERT-C3needs-confirmation — verbatim 미확인 toss 공식 SLASH 발표/페이지 재발굴 또는 ca-tmpl 정책으로만 표현 needs-confirmation
metric naming 영문 dot-case 강제 가 toss 의 명시적 contract TOSS-ALERT-C6needs-confirmation — 출처 검증 실패 Micrometer 표준으로만 정당화하고 toss 인용은 제거 또는 격하 표현 needs-confirmation

관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)

/coverage 가 채우는 생성물 — 손으로 유지하지 않는다. 기준: rules/coverage-gate.md. governing_docs: wiki/projects/ca-tmpl/observability-log-metric-trace-runbook (§Metric documented-only). 상태: covered-here(이 브랜치 결정) / delegated(다른 owner 브랜치) / missing(아무도 안 맡음 → Blocking).

관심사 상태 owner 심각도 근거
Micrometer dot.case naming + Prometheus exporter covered-here D2 (governing §Metric:61)
Alert severity P1/P2/P3 분리 covered-here D7 + §P1/P2/P3 표 (governing §Metric:62)
Cardinality bound (userId/requestId unbounded label 금지) covered-here D8 + §Cardinality Bounds (governing §Metric:63)
SLO burn-rate vs traffic-based threshold 대안 결정 covered-here D3 + D5 (governing §Metric:64)
HTTP latency/error rate metric covered-here D2·D7 + §구현 가이드 §1 (http.server.requests)
dependency latency/error rate metric covered-here D2·D7 + §구현 가이드 §1 (dependency.client.requests)
JVM/process metric covered-here §구현 가이드 §1 (jvm.*, process.uptime)
metric naming/tag 기준 covered-here D2 + D8 + §Cardinality Bounds
DB pool metric delegated raw/branch-notes/feature-persistence-failure-baseline OK §구현 가이드 §1 consumed 표 (hikaricp.*)
retry/circuit breaker metric delegated raw/branch-notes/feature-outbound-http-client-baseline OK D4 + §구현 가이드 §1 consumed 표 (resilience4j.*)
exemplar/trace_id → metric label 대신 tracing 위임 delegated raw/branch-notes/feature-distributed-tracing-contract Should-fix D8 위임 링크 — 단 수신 브랜치 In-scope 에 exemplar 미명시(UNLINKED_DELEGATION 경계, 후속 /branch-spec feature-distributed-tracing-contract)

coverage-auditor 판정 (2026-06-14): Covered — Blocking 0 / Should-fix 1 (exemplar 위임 수신 브랜치 In-scope 보강) / Advisory 0.

마주친 문제

  • 아직 없음.

묶음

본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

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

  • (없음 — 2026-06-15 Task 1: shared-contract pure contract type 구현 완료. 컴파일/테스트 오류 없음.)

  • (없음 — 2026-06-15 Task 2: app-bootstrap runtime enforcement + contract test 구현 완료. 컴파일/테스트 오류 없음.)

  • [2026-06-15 Spec-reviewer fix] MetricsDistributionMeterFilter.SLO_DRIVEN_TIMERS 에서 dependency.client.requests 누락 (Req #10 PARTIAL). metrics.yaml 기준 이 행은 owner_branch: feature-metrics-alerting-contract + type: timer + histogram_buckets: slo_driven — 나머지 4개 owned timer 와 동일 D9 그룹. 원인: Task 2 초기 구현 시 spec §Histogram Buckets/Percentile "dependency call" 항목을 SLO_DRIVEN_TIMERS Set 에 추가하지 않음. 수정: SLO_DRIVEN_TIMERS 5개로 확장 + 단위 테스트 @ValueSource 5개로 확장 + MetricsAlertingContractTest에 registry↔filter drift guard 테스트(every_owned_slo_driven_timer_is_configured_by_distribution_filter) 추가 + 미사용 import java.util.Collection; 제거 + plan doc Task 2 item 2 수정. TDD red(2개 테스트 실패) → green(전체 suite PASS) 증명 완료.

  • [2026-06-15 Code-quality polish pass] 코드 품질 리뷰어 지적 4건 수정 (actually-implemented + locally-verified):

    • Important 1 (MetricsContractConfigTest D9 test): install_applies_slo_distribution_to_http_server_requests — 기존 단언(timer().isNotNull())은 MetricsDistributionMeterFilter 미설치 시에도 통과. timer.takeSnapshot().histogramCounts().isNotEmpty() 로 강화. percentilesHistogram(true) + serviceLevelObjectives(...) 조합이 실제로 SLO 버킷을 만들어야만 통과. @DisplayName 도 단언 내용에 맞게 수정.
    • Important 2 (MetricsContractConfigTest no-op test): config_is_noop_without_meter_registry — 기존 단언(config.isNotNull())은 @PostConstruct 경로를 전혀 호출하지 않음. installFilters() 가시성을 public→package-private 으로 낮추고, 테스트와 같은 패키지에서 assertThatCode(config::installFilters).doesNotThrowAnyException() 로 교체. NPE 회귀 시 실패함을 보장. DistributedTracingContractTest.tracing_sampling_rate_gauge_is_noop_without_meter_registry 선례 일치.
    • Minor 1 (MetricsAlertingContractTest): Collectors.toList() 2곳을 Stream.toList() (Java 21 immutable)로 교체. 미사용 import java.util.stream.Collectors; 제거.
    • Minor 2 (MetricsCardinalityMeterFilter, MetricsDistributionMeterFilter): stateless infrastructure leaf class 에 final 추가. MetricsContractConfig (@Configuration, CGLIB proxy) 는 손대지 않음.
    • Minor 4 (AlertSeverity, shared-contract — controller 직접 수정): inline java.util.Locale.ROOT FQN 2곳을 import java.util.Locale; + Locale.ROOT 로 정리 (파일 내 import 스타일 일관성). ./gradlew :shared-contract:test --rerun-tasks compileJava+test 재실행 BUILD SUCCESSFUL 로 확인.
    • Minor 3 (의도적 미변경): MetricsContractConfigfinal 로 만들지 않음 — @Configuration full-mode CGLIB proxy 가 필요하므로 final 시 context load 실패. 리뷰어도 동일 지적.
    • ./gradlew :app-bootstrap:test BUILD SUCCESSFUL (23 tasks).
  • controller 최종 검증 (2026-06-15): 리뷰 체인(ca-architect-sentinel ready / ca-spec-reviewer ready (Req #10 fix 후) / ca-quality-reviewer 지적 4건 수정) 완료 후 컨트롤러가 전체 검증 실행 — ./gradlew :shared-contract:test :app-bootstrap:test verifyCleanArchitectureDependencies = 594 tests / 594 pass / 0 fail / 0 skip, arch dependency check + CleanArchitectureTest(48) PASS. locally-verified 등급은 컨트롤러 검증 근거를 가짐 (자기 보고 아님). 커밋은 사용자가 직접 수행 — 작업 트리에만 변경 잔류.

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • Micrometer dot.case naming 과 Prometheus underscore naming 의 차이, 그리고 변환 시 exporter 가 자동 부착하는 suffix(_seconds_bucket 등)를 개발자가 직접 처리해야 하는지 여부.
  • metric label cardinality 폭발이 발생하는 원인과 user_id/request_id 가 metric label 로 금지되는 이유 (tracing exemplar 와 차이).
  • OptionalInt vs Optional<Integer> 선택 기준 (Java 원시 타입 boxing 비용 vs API 일관성).
  • Micrometer @Bean MeterFilter vs registry.config().meterFilter() 직접 설치 차이 — Spring Boot Actuator MeterRegistryCustomizer 없는 환경에서 @Bean MeterFilter 가 왜 무효인가.
  • publishPercentileHistogram (aggregable cross-instance) vs publishPercentiles (client-side non-aggregable) 차이 — Prometheus 다중 인스턴스 p99 집계 시 어떤 방식이 올바른가.
  • DistributionStatisticConfig.build().merge(config) 에서 .merge() 순서가 왜 중요한가 (caller config 우선 vs filter 우선).

부가 tooling 변경 (2026-06-15): 리팩토링 어드바이저 구조

본 절은 metrics/alerting 계약 자체가 아니라, 이 브랜치 작업 중 추가한 하네스 tooling(리팩토링 비평 에이전트 + 표준 SSOT)을 기록한다. metrics 코드는 이 구조의 첫 드라이런 대상이었다. 외부 표준 근거가 raw 에 미등록이므로 아래 설계 결정은 needs-confirmation 로 표기한다 (Decision Evidence Map 의 D1~D10 과 별개 — 본 절은 도구 결정).

변경 파일 (전부 markdown — Java/Gradle 동작 무변경)

  • 신규 .agents/plugins/ca-superpowers/rules/refactoring-standards.md — 리팩토링 판단 SSOT (D1 JavaDoc 계약표면한정 / D2 네이밍 / D3 구조 / D4 계약타입 형태). 등급: actually-implemented
  • 신규 .claude/agents/ca-refactor-advisor.md — 기존 커밋 코드 선제 스윕 → docs/superpowers/plans/ 에 행위보존 plan 작성. read-only on src/**, verdict 미게이트. 등급: actually-implemented
  • 수정 .claude/skills/ca-superpowers-workflow/SKILL.md — Subagent Lanes + Dispatch Tree 에 리팩토링 스윕 분기. 등급: actually-implemented
  • 수정 .claude/agents/ca-quality-reviewer.md — mandatory reads + G1 표에 표준 문서 연결(SSOT 공유). 등급: actually-implemented
  • 산출물 docs/superpowers/plans/2026-06-15-metrics-refactor-plan.md — 드라이런이 생성한 metrics 리팩토링 plan (P1=1/P2=1/P3=1, 전부 D1). 등급: plan 은 actually-implemented, 리팩토링 실행 자체는 planned

도구 설계 결정 (사용자 대화형 선택 — 별도 Decision-ID 체계)

  • RD1: 표준 문서 먼저 명문화 후 에이전트가 참조 (vs 에이전트 내부 판단 / 기존 reviewer 확장). 이유: "naming 미명문화"가 근본 원인 → 객관 기준 SSOT 필요. 근거: 사용자 선택 + Google Java Style Guide(객관 표준) — needs-confirmation (raw 미등록)
  • RD2: JavaDoc 정책 = 계약 표면에만 (vs 공개 API 전부 / 전면 최소화). 이유: 스켈레톤에서 문서 가치가 가장 높은 곳은 템플릿 사용자가 의존하는 계약 표면. 근거: 사용자 선택 — needs-confirmation
  • RD3: 출력 = 실행 가능 plan 파일 → ca-implementer 위임 (vs findings 리포트 / 자동 plan화). 이유: 기존 plan→implementer 머신 재사용. 근거: 사용자 선택 + 기존 리뷰 체인 패턴
  • RD4: verdict 게이트 미편입 (독립 어드바이저). 근거: ca_verdict_gate.py:166-167 이 미등록 agent_type 을 emit_allow() 로 통과 (코드 확인) — 훅 무수정
  • RD5: 어드바이저가 plan 파일 직접 Write (docs/superpowers/plans/ 한정, src/** 금지). 근거: 사용자 선택 (왕복 최소)

검증

  • 구조 검증: 4파일 grep 통과 (D1~D4 4헤더 / agent Write 포함·verdict 0 / SKILL 2곳 / reviewer 2곳).
  • 통합 드라이런: ca-refactor-advisor 계약을 metrics 스코프에 실행 → 유효 plan 생성, src/** 무수정 확인, verdict 블록 없음, 모든 file:line sed/grep 검증. 라이브 subagent_type 디스패치는 세션 리로드 후 가능 — 정의가 세션 시작 시점 레지스트리에 없어 fallback(general-purpose 에 정의 파일 준수)으로 계약 검증.
  • Java/Gradle: 동작 무변경이라 테스트 미실행 (해당 없음).

Gotchas (재사용 가능한 도구 마찰)

  • .claude/agents/*.md세션 시작 시 로드된 레지스트리에만 등록 → 생성 직후 같은 세션에서 subagent_type 으로 디스패치 불가. 리로드 필요.
  • ca-tmpl 세션의 wiki_claim_gate PreToolUse 훅이 echo 문자열 안의 >=/> 를 shell 리다이렉트로 오인해 무해한 grep Bash 를 차단 → > 문자를 피해 재실행으로 우회.

Cluster (이 부가 작업 한정)

  • Errors: 위 Gotchas 2건 (별도 raw/errors/ 노트는 선택 — 필요 시 canonical 추출).
  • Interview prep: "새 subagent 를 세션 중 추가했을 때 즉시 디스패치되지 않는 이유(레지스트리 로드 타이밍)" / "리팩토링 비평을 객관 표준 SSOT 로 분리하는 설계 이점".
  • Blog topics: "Clean Architecture 스켈레톤에서 리팩토링 어드바이저 + implementer 위임 구조 설계" — branch note 외 별도 글감 가능, 현재 미작성.

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 nav 유지.

  • (없음 — Phase E 설계 단계. C2 구현 진입 시 daily note 연결)

완료 후 정리

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목:
    • locally-verified 항목:
    • prod-verified 항목:
  • 추출하지 않을 항목 (planned / documented-only / abandoned):