Micrometer 4편. 태그가 곧 차원이 되는 방식, common tags로 전 메트릭에 application·env·region을 일괄 주입하는 방법, 카디널리티 폭발이 Prometheus를 죽이는 원리, URI는 반드시 템플릿으로 정규화해야 하는 이유, MeterFilter의 deny·maximumAllowableTags로 방어하는 패턴, Micrometer 네이밍 컨벤션까지.
이 글은 Micrometer 입문에서 운영까지 시리즈 4편이에요. 3편에서 Timer 의 percentile 과 histogram 을 깊이 다뤘고, 그 마지막에서 "태그 cardinality 를 점검해야 한다"는 말이 슬쩍 나왔어요. 이번 글이 그 이야기예요. 3편까지는 "무엇을 어떻게 재냐"였다면, 4편은 "재고 난 데이터를 어떤 차원으로 쪼개 보느냐, 그리고 그 쪼개기가 치명적으로 잘못됐을 때 어떤 일이 생기냐"를 다뤄요.
카디널리티 폭발은 Prometheus OOM 의 1순위 원인 중 하나예요. 현장에서 이걸 경험하면 Prometheus 가 갑자기 죽고, 알람이 멈추고, 그제야 "태그에 뭘 넣었더라"를 뒤지게 되죠. 미리 구조를 잡아두면 그 사고는 피할 수 있어요.
이번 글의 범위
이번 글은 태그 설계와 카디널리티 방어를 다뤄요.
| 자리 | 내용 |
|---|---|
| 태그 = 차원 | 같은 메트릭 이름에 태그 조합마다 별도 시계열 생성 |
| common tags | application·env·region을 MeterRegistryCustomizer로 일괄 주입 |
| 카디널리티 폭발 | 태그 값 종류 수 × 메트릭 = 시계열 수. 폭발 조건과 결과 |
| URI 템플릿 | raw path 금지, Spring Boot 자동 정규화 방식 |
| MeterFilter | deny·accept·rename·replaceTagValues·maximumAllowableTags |
| naming convention | dot.notation → 백엔드별 변환 규칙 |
태그가 곧 차원이다
같은 메트릭 이름이라도 태그가 다르면 Prometheus 에서는 완전히 다른 시계열이 돼요. 이게 dimensional metrics 의 핵심이고, Micrometer 가 "태그를 차원"이라고 부르는 이유예요.
// 같은 이름 "orders.created" — 태그 조합마다 별도 시계열
Counter.builder("orders.created")
.tag("region", "ap-northeast-2")
.tag("type", "online")
.register(registry)
.increment();
Counter.builder("orders.created")
.tag("region", "us-east-1")
.tag("type", "offline")
.register(registry)
.increment();
Prometheus 가 이 두 카운터를 긁어가면 서로 다른 레이블 조합으로 저장돼요.
orders_created_total{region="ap-northeast-2", type="online"} 142
orders_created_total{region="us-east-1", type="offline"} 37
이 두 시계열이 있으면 sum by (region) 으로 리전별 합산이 가능하고, sum by (type) 으로 채널별 합산도 돼요. 태그 하나가 분석의 축 하나가 되는 거예요. 이게 "태그 = 차원"이 주는 힘이에요.
코드에서 중요한 건 tag(key, value) 의 value 가 항상 유한하고 예측 가능한 집합에서 나와야 한다는 거예요. "ap-northeast-2", "us-east-1", "eu-west-1" 처럼 몇 가지 값만 도는 집합이면 괜찮아요. userId, requestId, email 처럼 요청마다 다른 값이 들어오면 카디널리티 폭발이 시작돼요. 이건 뒤에서 자세히 다룰게요.
common tags — 전 메트릭 공통 라벨
application, env, region, instance 같은 태그는 이 앱에서 나가는 모든 메트릭에 붙어야 해요. 메트릭이 수십 개면 각 Meter 에 일일이 붙이는 건 비현실적이죠. 레지스트리 레벨에서 한 번만 설정하면 끝이에요.
registry.config().commonTags
registry.config().commonTags(
"application", "order-service",
"env", "production",
"region", "ap-northeast-2",
"instance", System.getenv("HOSTNAME")
);
이렇게 하면 이후 이 레지스트리에 등록되는 모든 Meter 에 저 네 태그가 자동으로 붙어요. Counter.builder("orders.created").register(registry) 한 줄만 쓰면 내부적으로 저 태그들이 이미 들어가 있어요.
Spring Boot 에서의 방법
Spring Boot 환경에서는 MeterRegistryCustomizer 빈으로 더 깔끔하게 쓸 수 있어요.
@Bean
MeterRegistryCustomizer<MeterRegistry> commonTagsCustomizer(
@Value("${spring.application.name}") String appName) {
return registry -> registry.config()
.commonTags(
"application", appName,
"env", System.getenv().getOrDefault("ENV", "local"),
"region", System.getenv().getOrDefault("AWS_REGION", "unknown")
);
}
application.yml 에서도 설정할 수 있어요.
management:
metrics:
tags:
application: ${spring.application.name}
environment: ${ENV:local}
region: ${AWS_REGION:local}
YAML 설정과 Java 코드 방식은 최종적으로 같은 효과예요. 팀 표준을 하나로 맞추면 충분해요.
common tags 로 넣기 좋은 후보는 이 정도예요.
| 태그 키 | 좋은 값 예시 | 용도 |
|---|---|---|
application |
order-service |
서비스 이름 — 여러 서비스 메트릭 구분 |
env |
production, staging, local |
환경 구분 |
region |
ap-northeast-2 |
리전별 분석 |
instance |
Pod 이름, 호스트명 | 인스턴스별 드릴다운 |
version |
1.4.2 |
배포 버전 — 버전별 성능 비교 |
여기서 instance 는 주의가 필요해요. Pod 가 수백 개이고 이름이 매번 달라지는 환경이라면, instance 태그가 카디널리티를 크게 올릴 수 있어요. Prometheus 는 instance 레이블을 스크랩 대상의 IP/포트로 자동 부여하기 때문에, 굳이 앱에서 중복으로 넣을 필요가 없는 경우도 많아요. 환경에 맞게 판단해야 해요.
카디널리티 폭발이란
카디널리티는 태그 값의 고유 조합 수예요. 직접 계산해 볼게요.
메트릭: orders.created
태그 1 (region): ap-northeast-2 · us-east-1 · eu-west-1 → 3가지
태그 2 (type): online · offline → 2가지
태그 3 (status): success · failure → 2가지
총 시계열 수 = 3 × 2 × 2 = 12개
12개는 아무 문제 없어요. Prometheus 가 수백만 시계열을 처리하는 시스템이니까요. 그러면 뭐가 문제일까요?
메트릭: orders.created
태그 1 (region): ap-northeast-2 · us-east-1 · eu-west-1 → 3가지
태그 2 (userId): 사용자마다 다름 → 100만 가지
총 시계열 수 = 3 × 1,000,000 = 3,000,000개
userId 하나를 태그로 넣는 순간 시계열이 3백만 개로 폭증해요. 메트릭이 10개라면 3천만 개가 되는 거고, 각 시계열은 Prometheus 메모리에 상주하기 때문에 결국 OOM 으로 죽어요.
절대 태그로 쓰면 안 되는 값들 — userId, email, requestId, sessionId, orderId, timestamp, raw URL, IP 주소.
이런 값은 요청마다 고유하거나 무한대에 가까운 집합이에요. 태그에 한 번 넣으면 Prometheus 메모리가 선형으로 늘어나고, 스크랩이 느려지다가 결국 Prometheus 가 죽어요. 발견하면 즉시 제거해야 해요.
카디널리티 폭발이 무서운 이유가 하나 더 있어요. 스크랩이 느려진다는 거예요. Prometheus 가 /actuator/prometheus 를 긁을 때 시계열 수가 많으면 응답 바이트가 커지고, 스크랩 타임아웃이 발생하고, 수집 주기를 맞추지 못해요. 메트릭이 듬성듬성 수집되거나 아예 빠지기 시작해요. 알람이 오다 말다 하는 증상이 나타나요.
URI는 반드시 템플릿으로
카디널리티 폭발에서 가장 자주 보이는 패턴이 URI 태그예요. Spring Boot 는 기본적으로 http.server.requests 메트릭에 uri 태그를 붙여요. 이게 의도대로 동작하면 괜찮은데, 잘못 설정하면 주문 건수만큼 시계열이 생겨요.
❌ raw path 로 들어왔을 때 (위험):
http_server_requests_seconds{uri="/order/10001"} ...
http_server_requests_seconds{uri="/order/10002"} ...
http_server_requests_seconds{uri="/order/10003"} ...
... (주문 수 = 시계열 수)
✅ template 로 정규화됐을 때 (안전):
http_server_requests_seconds{uri="/order/{id}"} ...
Spring Boot 는 @PathVariable 이 있는 엔드포인트를 자동으로 path template 로 정규화해요. @GetMapping("/order/{id}") 로 선언한 컨트롤러라면, Micrometer 가 uri 태그를 /order/{id} 로 기록해요. 실제 값 10001, 10002 가 들어가지 않아요.
@GetMapping("/order/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
// 이 엔드포인트의 http.server.requests 메트릭은 uri="/order/{id}" 로 기록됨
return ResponseEntity.ok(orderService.find(id));
}
문제가 생기는 경우는 @RequestMapping 에 wildcard 를 쓰거나, 직접 HttpServletRequest.getRequestURI() 를 태그 값으로 넣는 경우예요. 또는 404 응답이 문제가 되는 경우도 있어요. 없는 경로로 요청이 오면 Spring 이 /error 또는 원래 요청한 경로를 uri 태그에 담는데, 이게 공격성 스캐너나 봇 트래픽이 많을 때 예기치 않게 시계열을 만들 수 있어요.
# application.yml — Spring Boot 의 uri 태그 정규화 기본값
management:
metrics:
web:
server:
request:
autotime:
enabled: true
Spring Boot 가 자동으로 추가하는 http.server.requests 메트릭의 uri 태그는 uri="/UNKNOWN" 을 공용 fallback 으로 쓰도록 내부 설정이 되어 있어요. 직접 커스텀 Timer 에 uri 태그를 추가할 때는 반드시 template 을 직접 계산해서 넣어야 해요.
MeterFilter로 방어하기
태그를 잘못 넣는 코드를 모든 팀원이 완벽하게 지키기는 어렵고, 외부 라이브러리가 자동으로 만들어 주는 Meter 가 예상치 못한 태그를 달고 나오기도 해요. 이럴 때 MeterFilter 가 레지스트리 레벨에서 메트릭을 가로채서 필터링·변환할 수 있어요.
registry.config().meterFilter( /* 필터 구현체 */ );
자주 쓰는 MeterFilter 패턴을 하나씩 볼게요.
deny / accept
// 특정 이름 prefix 의 메트릭 차단
registry.config().meterFilter(
MeterFilter.denyNameStartsWith("jvm.internal")
);
// 특정 이름만 허용 (나머지 차단)
registry.config().meterFilter(
MeterFilter.accept(id -> id.getName().startsWith("orders."))
);
MeterFilter.deny(); // 이 필터 뒤에 등록한 것들 전부 차단
필터는 등록 순서대로 평가돼요. accept 다음에 deny 를 등록하면, accept 가 참인 Meter 는 살고 나머지는 차단되는 구조로 쓸 수 있어요.
replaceTagValues — 태그 값 정규화
특정 태그의 값을 변환하거나 고카디널리티 값을 집약할 때 써요.
// uri 태그에서 숫자 ID 가 들어온 경우 template 로 치환
registry.config().meterFilter(
MeterFilter.replaceTagValues(
"uri",
uri -> uri.matches(".*\\d+.*") ? "/resource/{id}" : uri
)
);
maximumAllowableTags — 카디널리티 상한선
가장 직접적인 방어선이에요. 특정 Meter 의 특정 태그 키에 허용 가능한 고유 값 수의 상한을 걸어요.
registry.config().meterFilter(
MeterFilter.maximumAllowableTags(
"http.server.requests", // 메트릭 이름
"uri", // 태그 키
100, // 최대 허용 고유 값 수
MeterFilter.deny() // 초과 시 적용할 필터 (여기서는 차단)
)
);
100번째 고유 uri 태그 값까지는 시계열을 만들고, 101번째부터는 해당 시계열을 차단해요. 완벽한 방어는 아니지만(100개의 쓸모없는 시계열은 생길 수 있음), 무한정 폭발하는 건 막아요.
이름 변환 (rename, map)
// 이름 rename
registry.config().meterFilter(
MeterFilter.renameTag("http.server.requests", "uri", "endpoint")
);
// MeterFilter 직접 구현 — 특정 태그 제거
registry.config().meterFilter(new MeterFilter() {
@Override
public Meter.Id map(Meter.Id id) {
return id.withTag("env", "production"); // 태그 추가
}
});
여러 필터 조합
@Bean
MeterRegistryCustomizer<MeterRegistry> meterFilters() {
return registry -> registry.config()
.meterFilter(MeterFilter.deny(id ->
id.getName().startsWith("jvm.internal"))) // jvm 내부 메트릭 차단
.meterFilter(MeterFilter.maximumAllowableTags(
"http.server.requests", "uri", 50,
MeterFilter.deny())) // uri 카디널리티 상한
.meterFilter(MeterFilter.replaceTagValues(
"uri",
v -> v.matches("/order/\\d+") ? "/order/{id}" : v // 숫자 ID 정규화
));
}
필터 체인을 하나의 MeterRegistryCustomizer 에 모아서 관리하면 나중에 찾기 편해요. 분산해서 여러 빈에 등록하면 순서가 꼬일 수 있어요.
naming convention
Micrometer 가 권장하는 이름 규칙은 소문자 dot-separated 예요.
orders.created ✅ 권장
orders_created ❌ (underscore)
ordersCreated ❌ (camelCase)
orders-created ❌ (dash)
코드에서 이 이름으로 등록하면 Micrometer 가 각 백엔드 레지스트리에 맞는 이름으로 자동 변환해요.
Micrometer 등록 이름: http.server.requests
Prometheus 로 노출: http_server_requests_seconds
(dot → underscore, 단위 suffix 자동 추가)
Datadog 로 노출: http.server.requests
(그대로, Datadog 은 dot 을 지원)
CloudWatch 로 노출: HttpServerRequests
(CamelCase 변환)
각 백엔드 레지스트리가 자기 관례로 변환해 주기 때문에 앱 코드에서는 Micrometer 규칙만 따르면 돼요.
한 가지 주의할 점이 있어요. 태그 키도 소문자 dot-separated 를 권장해요. userId 대신 user.id, httpMethod 대신 http.method 식으로요. 실제로 Spring Boot 의 자동 계측 메트릭들(http.server.requests, jvm.memory.used 등)이 모두 이 규칙을 따르고 있어요.
단위 suffix 규칙도 있어요. Timer 는 Prometheus 에서 항상 _seconds suffix 가 붙고, DistributionSummary 는 단위가 없어서 suffix 가 없어요. Counter 는 _total 이 붙어요. 이 suffix 를 이름에 직접 박지 마세요.
// ❌ 이름에 단위 직접 박기 (중복)
Timer.builder("http.server.requests.seconds") // 실제론 _seconds_seconds 가 됨
// ✅ 단위 없이
Timer.builder("http.server.requests") // Prometheus 가 _seconds 자동 추가
.register(registry);
함정 정리
사고 1: userId 를 태그로 사용
사용자별 요청 수를 추적하려고 tag("userId", userId) 를 Counter 에 박아요.
원인 — "사용자별로 구분해야 하는 것 아니야?" 라는 직관에서 비롯됐어요. 메트릭과 로그의 역할이 헷갈린 거예요.
해결 — 메트릭은 집계 단위로 써야 해요. 사용자별 추적은 로그·분산 트레이스의 영역이에요. 대신 tier("premium"), userType("admin") 처럼 유한 집합의 집약값을 태그로 써요. 특정 사용자 행동은 로그에서 찾으면 돼요.
사고 2: raw URL 을 수동으로 태그에 넣음
HttpServletRequest.getRequestURI() 를 그대로 가져와 커스텀 Timer 태그에 박아요.
원인 — "어느 경로가 느린지 보고 싶어서" 라는 정당한 목적인데 구현이 잘못된 경우예요.
해결 — Spring 의 @PathVariable 이 있는 컨트롤러라면 HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE 를 request 에서 읽어 template 를 가져올 수 있어요. 또는 URI template 를 직접 상수로 넣는 게 가장 안전해요.
사고 3: 404 에러 경로가 시계열을 만듦
공격성 스캐너나 봇이 /admin, /wp-login.php, /actuator/env 같은 경로를 마구 때리는데, 이게 http.server.requests 의 uri 태그에 각각 쌓여요.
원인 — Spring Boot 의 uri 태그 자동 처리가 404 경로를 raw uri 로 기록하는 경우가 있어요.
해결 — MeterFilter 로 404 uri 태그를 /UNKNOWN 또는 특정 fallback 으로 교체하거나, 404 메트릭 자체를 차단해요. 또는 maximumAllowableTags 로 uri 상한을 설정해요.
사고 4: MeterFilter 순서가 꼬임
deny 와 accept 를 섞어 쓰는데 기대한 것과 반대로 동작해요.
원인 — MeterFilter 는 등록 순서대로 체인으로 평가해요. accept 가 deny 뒤에 있으면 deny 에서 걸리고 accept 는 평가 자체가 안 돼요.
해결 — accept 를 먼저 등록하고 deny 를 나중에 등록해야 의도대로 동작해요. 또는 MeterFilterReply.NEUTRAL 을 활용해서 다음 필터로 넘기는 방식을 명확히 이해하고 설계해야 해요.
사고 5: common tags 에 instance 를 과하게 넣음
모든 Pod 에 고유한 Pod 이름을 instance common tag 로 박아요.
원인 — "인스턴스별로 드릴다운하고 싶다"는 정당한 요구인데, 카디널리티와의 트레이드오프를 고려하지 않은 거예요.
해결 — Prometheus 는 이미 스크랩 대상 레이블로 instance 를 가지고 있어요. 앱에서 중복으로 넣으면 카디널리티만 올라가고 이점은 없어요. Prometheus recording rule 이나 Grafana 에서 instance 레이블을 활용하면 돼요. 꼭 앱에서 넣어야 한다면 환경(production/staging)이나 cluster 이름처럼 cardinality 가 낮은 값으로 대체해요.
사고 6: 이름에 단위를 직접 박음
Timer.builder("api.latency.ms") 처럼 이름에 단위를 박아요.
원인 — "단위를 이름에 명시하면 헷갈리지 않겠지" 하는 의도예요.
해결 — Micrometer 의 각 백엔드 레지스트리가 단위 suffix 를 자동으로 붙여요. Timer 는 Prometheus 에서 _seconds 가 붙어서 api.latency.ms.seconds 처럼 어색한 이름이 돼요. 이름에는 단위를 넣지 말고, Timer 빌더의 .baseUnit() 이나 DistributionSummary 빌더의 .baseUnit("bytes") 로 단위 정보를 제공해요.
사고 7: MeterFilter 를 여러 빈에 분산 등록
MeterRegistryCustomizer 빈을 여러 개 만들어서 각각 다른 filter 를 등록해요.
원인 — "역할별로 나눠서 관리하는 게 깔끔하다"는 의도예요. 작은 규모에서는 문제가 없는데 filter 간 순서가 보장되지 않아요.
해결 — filter 체인은 순서가 중요하기 때문에 한 곳에 모아서 명시적 순서대로 등록해요. 여러 빈이 필요하다면 @Order 어노테이션으로 빈 순서를 명시해야 해요.
사고 8: 너무 많은 태그를 한 Meter 에 박음
한 Counter 에 region, type, status, version, featureFlag, channel 까지 태그 6개를 달아요.
원인 — "나중에 어떤 차원으로 슬라이스할지 모르니 다 넣자"는 방어적 설계예요.
해결 — 태그 조합 수를 직접 계산해보면 됩니다. 리전 3 × 타입 4 × 상태 2 × 버전 10 × 피처플래그 5 × 채널 3 = 3,600 시계열. 메트릭이 10개면 36,000 시계열이에요. 진짜 쿼리에서 쓸 차원만 골라서 3~4개로 줄여야 해요.
사고 9: replaceTagValues 를 regex 없이 구현
태그 값 변환 로직을 MeterFilter 없이 서비스 코드 안에서 직접 처리해요.
원인 — MeterFilter 를 몰랐거나, 레지스트리 레벨이 아닌 비즈니스 로직 안에서 처리하려는 경우예요.
해결 — 태그 값 변환은 중앙화가 핵심이에요. 서비스 코드 여러 곳에 흩어진 변환 로직은 누락이 생기기 쉬워요. replaceTagValues 를 레지스트리 레벨에서 한 번만 등록하면 모든 Meter 에 일관 적용돼요.
사고 10: maximumAllowableTags 의 onMax 를 잘못 설정
maximumAllowableTags(name, tagKey, max, MeterFilter.deny()) 를 설정했는데 초과 메트릭이 완전히 사라지지 않고 이상하게 동작해요.
원인 — onMax 는 초과 분에만 적용되는 필터예요. 100개까지는 정상 기록되고, 101번째부터 onMax 가 적용돼요. deny() 면 101번째 이상 태그 값의 시계열 자체가 생기지 않아요. 문제는 이미 100개 시계열이 생긴 뒤 초과분은 기록 안 되니 "101번째 이상 요청의 카운트가 사라진다"는 현상이 생겨요.
해결 — maximumAllowableTags 는 시계열 폭증 방어용이지, 완벽한 데이터 수집 보장이 아니에요. 태그 값 자체를 설계 단계에서 유한 집합으로 제한하는 게 근본 해결책이에요. maximumAllowableTags 는 2차 방어선으로 써요.
운영 권장 패턴
Pattern 1: common tags 팀 표준화
@Bean
MeterRegistryCustomizer<MeterRegistry> teamStandardTags(
@Value("${spring.application.name}") String appName,
@Value("${spring.profiles.active:local}") String env) {
return registry -> registry.config()
.commonTags(
"application", appName,
"env", env,
"region", System.getenv().getOrDefault("AWS_REGION", "local")
);
}
팀 전체가 같은 세 태그를 쓰면 Grafana 에서 서비스 간 비교가 쉬워져요. application 태그가 없으면 여러 서비스를 한 Prometheus 에 수집할 때 어느 메트릭이 어느 서비스 것인지 구분이 어려워요.
Pattern 2: URI 카디널리티 방어
@Bean
MeterRegistryCustomizer<MeterRegistry> uriCardinalityGuard() {
return registry -> registry.config()
// 1차 — uri 값 정규화 (숫자 경로 템플릿화)
.meterFilter(MeterFilter.replaceTagValues(
"uri",
v -> v.matches(".*/\\d+(/.*)?") ? v.replaceAll("/\\d+", "/{id}") : v
))
// 2차 — 그래도 100개 초과 시 차단
.meterFilter(MeterFilter.maximumAllowableTags(
"http.server.requests", "uri", 100, MeterFilter.deny()
));
}
1차로 정규화, 2차로 상한 차단을 이중으로 걸어두면 안전해요.
Pattern 3: 불필요한 메트릭 차단으로 시계열 줄이기
@Bean
MeterRegistryCustomizer<MeterRegistry> metricDenyList() {
return registry -> registry.config()
.meterFilter(MeterFilter.denyNameStartsWith("jvm.gc.pause")) // GC 세부 pause
.meterFilter(MeterFilter.denyNameStartsWith("tomcat.servlet")) // Tomcat 내부
.meterFilter(MeterFilter.deny(id ->
id.getName().startsWith("spring.cloud.gateway") &&
id.getTag("routeId") != null)); // 게이트웨이 route 세부
}
Spring Boot Actuator 는 기본으로 JVM, Tomcat, HTTP, DB 풀 등 수십 개의 메트릭을 자동 생성해요. 팀에서 실제로 쓰지 않는 메트릭은 차단하는 게 Prometheus 메모리 절약에 효과적이에요.
Pattern 4: 태그 설계 검증 체크리스트
새 Meter 를 추가하기 전에 스스로 확인하는 체크리스트예요.
□ 태그 키마다 가능한 값의 집합을 나열해봤나?
→ 나열 불가 = 카디널리티 위험, 다른 값 찾기
□ 태그 값 조합의 최대 시계열 수를 계산해봤나?
→ 메트릭당 1,000 초과면 경고, 10,000 초과면 위험
□ 같은 정보를 로그나 트레이스로 대신할 수 있나?
→ 사용자별 추적은 로그, 개별 요청 추적은 트레이스
□ common tags 와 Meter-level 태그가 중복되지 않나?
→ application, env 는 common tags 에서만
Pattern 5: 네이밍 컨벤션 자동 검증
// 커스텀 MeterFilter 로 이름 규칙 강제
@Bean
MeterRegistryCustomizer<MeterRegistry> namingConventionGuard() {
return registry -> registry.config()
.meterFilter(new MeterFilter() {
@Override
public Meter.Id map(Meter.Id id) {
// camelCase 또는 underscore 감지
String name = id.getName();
if (name.contains("_") || name.chars().anyMatch(Character::isUpperCase)) {
// 실제 운영에서는 경고 로그 + 개발 환경에서는 exception
log.warn("Non-standard meter name detected: {}", name);
}
return id;
}
});
}
개발 환경에서는 이름 규칙 위반을 바로 잡을 수 있어요. SimpleMeterRegistry + 이 필터로 단위 테스트를 구성하면 CI 에서 잡을 수도 있어요.
Pattern 6: 카디널리티 현황 주기적 점검
-- Prometheus 에서 시계열 수 상위 10개 메트릭
topk(10, count by (__name__)({__name__=~".+"}))
이 쿼리를 Grafana 대시보드에 박아 두면 카디널리티가 높은 메트릭을 주기적으로 파악할 수 있어요. 갑자기 시계열 수가 올라가는 메트릭이 있으면 배포 때 어떤 코드가 들어갔는지 바로 추적할 수 있어요. Micrometer 공식 docs 에서 MeterFilter API 전체 레퍼런스를 확인할 수 있어요.
시험 직전 한 번 더 — 카디널리티 압축 노트
태그 = 차원
- 같은 메트릭 이름 + 태그 조합 → 별도 시계열 생성
- 태그 값은 반드시 유한하고 예측 가능한 집합에서
- 태그로 슬라이스 → Prometheus 에서 집계 쿼리 가능
common tags
registry.config().commonTags(key, value, ...)로 전 메트릭에 일괄 추가- Spring Boot 에서는
MeterRegistryCustomizer빈 또는management.metrics.tagsYAML - application · env · region → 팀 표준 3종
- instance 는 Prometheus 가 이미 갖고 있어서 중복 주의
카디널리티
- 카디널리티 = 태그 값의 고유 조합 수
- 시계열 수 = 메트릭당 (태그1 값 수 × 태그2 값 수 × …)
- 절대 금지 태그 값: userId · email · requestId · sessionId · timestamp · raw URL
- 카디널리티 폭발 → Prometheus 메모리 OOM + 스크랩 느려짐 + 알람 누락
URI 태그
- raw path(
/order/12345) → 주문 수 = 시계열 수 (폭발) - path template(
/order/{id}) → 시계열 1개 - Spring Boot 는
@PathVariable엔드포인트 자동 정규화 - 직접 uri 태그 쓸 때는 template 값 계산해서 넣을 것
MeterFilter
registry.config().meterFilter(...)로 레지스트리 레벨 등록deny()/denyNameStartsWith(prefix)— 메트릭 차단accept()— 특정 메트릭만 허용 (다음 deny 와 조합)replaceTagValues(tagKey, fn)— 태그 값 변환 · 집약maximumAllowableTags(name, tagKey, max, onMax)— 카디널리티 상한 방어- 필터는 등록 순서대로 체인 평가 — 순서 중요
naming convention
- Micrometer 권장: 소문자 dot-separated (
http.server.requests) - underscore · CamelCase · dash 는 Micrometer 레벨에서 쓰지 않음
- 각 백엔드가 자동 변환: Prometheus는 underscore + suffix(
_seconds,_total) - 이름에 단위 직접 박지 말 것 — 레지스트리가 suffix 자동 추가
흔한 사고 목록
- userId · email 등 무한대 값 태그 → 카디널리티 폭발
- raw URL 태그 → 요청 수 = 시계열 수 폭발
- MeterFilter accept/deny 순서 꼬임 → 기대와 반대 동작
- common tags 에 instance 중복 → 불필요한 카디널리티 상승
- 태그 6개 이상 한 번에 → 조합 폭발 (미리 곱해볼 것)
- 이름에 단위 박음 (
latency.ms) → Prometheus 에서latency_ms_seconds생성
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 1편 — 메트릭의 벤더 중립 facade·큰 그림
- 2편 — Meter 타입 깊이 (Counter·Gauge·Timer·DistributionSummary)
- 3편 — Timer·percentile·histogram·SLO 깊이
다음 글: