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

459 lines
48 KiB
Markdown

---
title: branch / feature-metrics-alerting-contract
source_type: branch-note
status: raw
branch: feature-metrics-alerting-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]
tags: [branch, ca-skeleton, metrics, alerting, observability]
created: 2026-05-22
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-019
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-019
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: 3b83f7a53dc82997a9f9d3ff12f2106e16782bce65d5f95f8516a46532b0b2ee
---
# branch: feature-metrics-alerting-contract
> Layer: `raw/branch-notes/` — metrics와 alerting 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다. governing 문서는 [[wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]] (§Metric).
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: metric key·cardinality·alert contract 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다 | Work Item 완료 조건에 적용 | [[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 -->
## 목표
로그만으로 운영 감시는 부족합니다. skeleton은 HTTP, dependency, DB pool, JVM, retry/circuit breaker의 기본 metric과 `P1/P2/P3` alert severity를 가져야 합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- 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)
### 채택 결정 + 뒷받침
- 결정: **Micrometer dot.case naming + Prometheus exposition + P1/P2/P3 정량 threshold + cardinality bounds**.
- 뒷받침 source:
- [[raw/official-docs/metric-micrometer-naming-convention-official.md]] — Micrometer dot.case + unit suffix convention이 Spring Boot 3 default와 100% 일치함을 spec으로 확인.
- [[raw/company-tech-blogs/metric-toss-payments-alert-severity-techblog.md]] — 국내 fintech의 P1/P2/P3 운영 사례. 영문 dot-case naming 강제 + alert payload에 dashboard/log/runbook 링크 필수 정책이 ca-tmpl과 정합.
- [[raw/official-docs/metric-google-sre-slo-burn-rate.md]] — threshold alert가 baseline이고 burn-rate는 후속 도입이라는 ca-tmpl 결정이 SRE Workbook 전형적 진화 경로와 정합함을 확인.
### 검토 대안 + source
- 대안 1 — **OpenTelemetry metrics 직접 채택**: [[raw/official-docs/metric-otel-metrics-data-model-spec.md]]. naming 일부 다름(`http.server.request.duration` vs `http.server.requests`), Micrometer OTLP bridge 사용 시 swap 가능.
- 대안 2 — **SLO burn-rate alerting**: [[raw/official-docs/metric-google-sre-slo-burn-rate.md]]. SLO 정식 수립 후 도입 권장, 현재는 잠정 SLO p99=1s 기반 threshold.
### 비교 핵심 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` 승급 위치 |
## 진행 중 메모
- log field와 metric tag 이름은 가능한 한 일치시킵니다. (registry 각 행의 `log_field_mapping` 이 SSOT — log/metric 상관용, [[raw/branch-notes/feature-log-management-contract]] 와 정합)
## 결정 사항 (decisions)
- 2026-05-22: metrics/alerting을 log contract와 별도 branch로 분리.
- 2026-05-22: metric naming은 Micrometer naming default, log/trace key는 foundation registry를 소비.
- 2026-05-22: alert threshold는 임의 수치가 아니라 SLO/error budget 또는 documented operational default에 연결.
- 2026-05-22: retry/circuit breaker metric은 outbound branch의 Resilience4j default를 소비.
- 2026-05-22: metric naming convention = Micrometer dot.case default. unit suffix는 Micrometer convention(`.seconds`/`.bytes`/`.total`) 강제.
- 2026-06-14: (branch-spec 자동조사) D8 cardinality 금지 정책을 Prometheus/Micrometer 공식 문서로 격상 — `UNSUPPORTED_DECISION` 해소. 근거 [[raw/official-docs/metric-prometheus-label-cardinality-best-practices]], [[raw/official-docs/metric-micrometer-high-cardinality-tags-detector]]. (수치 상한 ≤200/≤50 등은 여전히 운영 가정.)
- 2026-06-14: (branch-spec 자동조사) D9 histogram 전략 정합 — Prometheus 환경에서 client-side `publishPercentiles` 는 non-aggregable. `publishPercentileHistogram`+`serviceLevelObjectives`(→ `histogram_quantile()` 집계)를 cross-instance source of truth 로 둔다. 근거 [[raw/official-docs/metric-micrometer-histogram-percentile-concepts]], [[raw/official-docs/metric-prometheus-histograms-vs-summaries-practices]]. registry `metrics.yaml` 가 두 방식을 동시 선언함을 surface.
- 2026-06-14: (branch-spec 자동조사) D10 alert payload — runbook + dashboard(monitoring console) 링크는 Google SRE 공식 지지로 격상([[raw/official-docs/metric-google-sre-workbook-on-call]], [[raw/official-docs/metric-google-ewaschuk-philosophy-on-alerting]]). log query 링크는 `operational-default` 로 격하 표기(SRE 문헌 직접 명문 없음).
## 결정-근거 매핑
> 각 결정이 어떤 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 | 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.yaml` 의 `owner_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`/`OutboundHttpResilienceConfig` 는 `actually-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 금지.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-outbound-http-client-baseline]] D4 — `resilience4j.*` metric(명/outcome enum) 정의 소유. 그 계약이 바뀌면 본 branch 의 alert severity 행 영향.
- [[raw/branch-notes/feature-persistence-failure-baseline]] — `hikaricp.*` metric + pool exhaustion threshold 소유. 본 branch 는 DB pool alert 기준만 consume.
- [[raw/branch-notes/feature-log-management-contract]] — log field ↔ metric tag 이름 일치 정책. registry 각 행 `log_field_mapping` 이 상관 SSOT. `log.appender.dropped.total` owner.
- [[raw/branch-notes/feature-contract-registry-governance]] — `metrics.yaml` 스키마 owner. 행 스키마(필수 키) 변경 시 본 branch 행 갱신.
- [[raw/branch-notes/feature-distributed-tracing-contract]] — high-cardinality(trace_id) 는 metric label 대신 exemplar/trace 로 위임(`MM-HCARD-C5`). exemplar 도입은 tracing 인프라 의존 → 추후.
- `ca-tmpl/docs/registries/error-codes.yaml` — `error_code` tag cardinality cap(100) 의 SSOT.
## 테스트 계약
- 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-C3` 는 `needs-confirmation` — verbatim 미확인 | toss 공식 SLASH 발표/페이지 재발굴 또는 ca-tmpl 정책으로만 표현 | `needs-confirmation` |
| metric naming 영문 dot-case 강제 가 toss 의 명시적 contract | `TOSS-ALERT-C6` 는 `needs-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.
## 마주친 문제
- 아직 없음.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/metric-toss-payments-alert-severity-techblog]]
- [[raw/official-docs/metric-google-ewaschuk-philosophy-on-alerting]]
- [[raw/official-docs/metric-google-sre-slo-burn-rate]]
- [[raw/official-docs/metric-google-sre-workbook-on-call]]
- [[raw/official-docs/metric-micrometer-high-cardinality-tags-detector]]
- [[raw/official-docs/metric-micrometer-histogram-percentile-concepts]]
- [[raw/official-docs/metric-micrometer-naming-convention-official]]
- [[raw/official-docs/metric-otel-metrics-data-model-spec]]
- [[raw/official-docs/metric-prometheus-histograms-vs-summaries-practices]]
- [[raw/official-docs/metric-prometheus-label-cardinality-best-practices]]
- [[raw/official-docs/outbound-resilience4j-vs-spring-retry]]
- [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]]
- [[raw/official-docs/resilience4j-micrometer-module]]
<!-- GENERATED: sources:end -->
> 본 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** (의도적 미변경): `MetricsContractConfig` 는 `final` 로 만들지 않음 — `@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):