본문 바로가기

Micrometer 레지스트리

미터 이름과 태그 규칙을 정하고 카운터·타이머·MeterFilter의 카디널리티를 관리합니다.

애플리케이션이 Prometheus 클라이언트 API에 직접 결합하면 백엔드를 바꾸거나 테스트 레지스트리를 쓰기 어렵습니다.

Micrometer는 미터를 만드는 계측 API와 백엔드별 레지스트리를 분리합니다.

코드는 MeterRegistry에 의미 있는 측정을 기록하고, Prometheus·OTLP 같은 구현이 각 백엔드의 이름 규칙과 전송 형식으로 변환합니다.

추상화가 메트릭 의미까지 대신 정해 주지는 않으므로 이름과 태그는 애플리케이션 계약으로 설계해야 합니다.

MICROMETER · ID AND SERIES BUDGET

미터 식별자는 이름과 태그이며 시계열 비용을 함께 검사한다

Micrometer가 백엔드를 분리해도 측정 의미와 타입·단위·태그 카탈로그는 애플리케이션 책임입니다. 이름/태그가 같은 미터에 서로 다른 계약을 섞지 않습니다.

계측 계약과 레지스트리 및 백엔드의 관계같은 이름과 태그는 미터 식별자이고 타입·단위·허용 값은 카탈로그 계약이다. 레지스트리를 주입해 등록하고 백엔드에 내보낸 이름과 추가 계열을 확인한다. MeterFilter 정책은 별도 구성이다. 원 와이드 SVG와 보조 목록은 숨김 지원으로 보존하며 모바일에는 같은 3노드2관계를 표시한다.
미터 카탈로그이름·태그·단위
연결 관계미터 카탈로그 · 이름·태그·단위 → MeterRegistry · 주입·등록·필터
MeterRegistry주입·등록·필터
연결 관계MeterRegistry · 주입·등록·필터 → 백엔드 · 내보낸 이름·계열
백엔드내보낸 이름·계열
계측 계약에서 레지스트리와 백엔드로카탈로그는 이름과 태그 및 타입 단위를 정의하고 레지스트리에 등록하며 백엔드가 내보낸 이름과 시계열을 검증한다.
미터 카탈로그이름·태그 + 타입·단위
연결 관계미터 카탈로그 · 이름·태그 + 타입·단위 → MeterRegistry · 주입·등록·filter
MeterRegistry주입·등록·filter
연결 관계MeterRegistry · 주입·등록·filter → 백엔드 · 내보낸 이름·시계열
백엔드내보낸 이름·시계열

MeterFilter 차단 정책은 별도 구성 · 본문 예제에 자동 적용되지 않음

  1. 카탈로그 → 레지스트리 → 백엔드

    이름·태그로 식별하고 타입/단위를 고정합니다. 레지스트리를 주입하며 filter 정책과 실제 내보낸 이름을 별도 검증합니다.

Counter · 누적 사건

양의 사건을 누적합니다. 현재 대기열처럼 감소하는 상태로 읽지 않고 프로세스 재시작을 쿼리에 반영합니다.

Timer · 완료 한 번

정상과 예외 모두 표본을 종료합니다. 개수와 전체 시간을 가지므로 같은 사건의 중복 카운터를 검토하고 재시도 전체/시도 경계를 나눕니다.

시계열 예산 · 조합 상한

50 경로 × 5 메서드 × 6 상태 × 3 영역 × 20 인스턴스 = 90,000 태그 조합 상한입니다. 발행 버킷·추가 계열까지 별도 산정합니다.

값과 ID 집합을 함께 검증

본문 SimpleMeterRegistry 예시는 accepted=2, rejected=1, 미터 2개입니다. 예상치 못한 태그·동적 이름과 증가하는 미터 수를 함께 확인합니다.

등록 전에 값 집합을 제한하고 filter 적용 순서를 검증합니다. SimpleMeterRegistry 성공만으로 Prometheus 이름·라벨·히스토그램 비용이 증명되지는 않습니다.


미터 식별자

board.entry.processed라는 이름만 같아도 outcome=accepted와 outcome=rejected는 서로 다른 시계열입니다.

태그 순서보다 키-값 집합이 중요하고, 같은 이름에 미터 타입이나 태그 키 집합을 뒤섞으면 레지스트리 백엔드에서 거부되거나 해석이 깨질 수 있습니다.

이름, 타입, 기반 단위, 설명, 허용 태그 키와 값 목록을 카탈로그로 관리합니다.

점 표기법은 Micrometer의 이식 가능한 이름에 적합하고 백엔드 레지스트리가 Prometheus 밑줄 같은 관례에 따라 변환합니다.

코드에서 백엔드 변환 이름을 미리 사용하지 않습니다.

단위를 미터 이름에 중복하는 대신 기반 단위와 설명을 제공하되 쿼리하는 실제 내보낸 이름을 통합 테스트에서 확인합니다.

src/main/java/board/metrics/EntryMetrics.java
package board.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;

public final class EntryMetrics {
    private final Counter accepted;
    private final Counter rejected;

    public EntryMetrics(MeterRegistry registry) {
        accepted = Counter.builder("board.entry.processed")
                .description("Completed entry processing attempts")
                .baseUnit("entries")
                .tag("outcome", "accepted")
                .register(registry);
        rejected = Counter.builder("board.entry.processed")
                .description("Completed entry processing attempts")
                .baseUnit("entries")
                .tag("outcome", "rejected")
                .register(registry);
    }

    public void accepted() {
        accepted.increment();
    }

    public void rejected() {
        rejected.increment();
    }
}

카운터는 음수를 기록하지 않고 프로세스 수명 동안 누적합니다.

현재 대기열 크기처럼 줄어드는 상태를 카운터 두 개의 차로 즉석 복원할지 게이지로 직접 볼지 데이터 유실과 초기화를 고려해 결정합니다.


레지스트리 백엔드

애플리케이션 컨텍스트에는 레지스트리가 하나 이상 있을 수 있습니다.

복합 레지스트리는 자식 레지스트리가 추가되기 전 기록의 의미와 동적 레지스트리 교체를 이해해야 합니다.

운영 환경은 백엔드 레지스트리를, 단위 테스트는 SimpleMeterRegistry를 주입해 같은 코드를 검증합니다.

전역 정적 레지스트리는 편리하지만 테스트 간 미터가 남고 의존성이 숨습니다.

생성자로 레지스트리를 받으면 서비스의 계측 의존성과 생명 주기가 명확합니다.

라이브러리는 레지스트리 빈을 임의로 새로 만들지 않고 사용자가 제공한 레지스트리에 등록합니다.


공통 태그 비용

애플리케이션, 영역, 환경 같은 낮은 카디널리티 차원을 공통 태그로 붙이면 전체 인스턴스 쿼리가 쉬워집니다.

인스턴스 ID까지 붙이면 시계열 수가 복제본 수만큼 늘지만 운영 상세 분석에는 필요할 수 있습니다.

사용자 ID, 게시글 ID, URL 쿼리, 예외 메시지처럼 값이 사실상 무한한 데이터는 절대 공통 태그가 아닙니다.

시계열 예산은 미터별 허용 태그 값의 곱으로 계산합니다.

엔드포인트 50 × 메서드 5 × 상태 6 × 영역 3 × 인스턴스 20이면 하나의 메트릭 계열이 최대 90,000개 시계열을 만들 수 있습니다.

실제 히스토그램 버킷까지 곱해 메모리와 저장소를 산정합니다.

예산을 초과한 상태는 대시보드가 조금 느려지는 정도가 아닙니다.

레지스트리가 계속 새 미터를 보관해 힙이 늘고, 수집 페이로드와 백엔드 수집량이 급증하며, 쿼리가 타임아웃될 수 있습니다.

이것은 관측 비용 증가와 조회 실패를 동시에 만드는 운영 문제입니다.

독립 확인: 형식 2개·결과 2개·영역 3개·인스턴스 4개가 모두 독립적으로 조합될 수 있고 limit=48입니다. 원 SeriesBudget.verify는 통과하나요? 여기에 entryId 1000개 차원을 추가하면 어떤 결과인가요? dimensions에 0을 넣어 검사를 통과하는 것은 유효한 예산인가요?

풀이 확인

2×2×3×4=48이고 원 비교는 >limit이므로 같은 48은 통과합니다. ID 차원이 추가되면 48000으로 초과합니다. 원 생성자는 각 차원의 양수를 검사하지 않으므로 0이나 음수 입력을 유효한 차원으로 허용해서는 안 됩니다. 양의 고유 값 수만 입력한다는 전제를 명시하고 그 입력 검증을 별도로 구현하세요. 곱셈 overflow는 Math.multiplyExact가 예외로 드러냅니다.

entryId 태그가 요청마다 달라져 등록된 미터 수가 지속 증가하면 CARDINALITY_LEAK으로 판정하고 해당 계측을 릴리스 전에 차단합니다.

시계열이 많은데 트래픽이 고정되어 있다면 값 집합과 미터 생성 스택을 함께 조사합니다.

cardinality failure 판별
requests=1000, bounded outcomes=3
registered series=1003
unexpected tag key=entryId
result=CARDINALITY_LEAK
src/main/java/board/metrics/SeriesBudget.java
package board.metrics;

import java.util.List;

public record SeriesBudget(String meterName, List<Integer> dimensions, long limit) {
    public SeriesBudget {
        dimensions = List.copyOf(dimensions);
        if (meterName.isBlank() || limit < 1) {
            throw new IllegalArgumentException("invalid series budget");
        }
    }

    public long maximumSeries() {
        long total = 1;
        for (int values : dimensions) {
            total = Math.multiplyExact(total, values);
        }
        return total;
    }

    public void verify() {
        if (maximumSeries() > limit) {
            throw new IllegalStateException("SERIES_BUDGET_EXCEEDED");
        }
    }
}

MeterFilter 정책

MeterFilter로 특정 미터를 거부하거나 태그 값 수를 제한하고 공통 태그, 이름 변경, 히스토그램 설정을 적용할 수 있습니다.

계측 코드가 이미 값 범위가 제한되지 않은 ID로 미터를 계속 생성한 뒤 백엔드에서 버리는 것보다 등록 시점에 차단하고 위반 메트릭과 로그를 남깁니다.

필터 순서도 결과에 영향을 줍니다.

이름 변경 뒤 거부 매처가 어느 ID를 보는지 테스트하고, 백엔드마다 다른 필터가 메트릭 규칙을 갈라놓지 않게 공유 관례를 둡니다.

차원 수의 곱은 가능한 태그 조합의 상한입니다. 모든 조합이 실제로 관측되어 등록됐다는 뜻은 아니며, 히스토그램의 버킷·count·sum 등 내보낸 추가 계열도 별도 산정합니다.

애플리케이션 시작 시 카탈로그에 없는 태그 키를 탐지하는 레지스트리 리스너도 고려합니다.


타이머 기록

타이머는 개수와 전체 시간, 백엔드 설정에 따라 최댓값과 히스토그램을 제공합니다.

시작과 종료를 수동 nanoTime으로 측정해 서로 다른 카운터에 기록하면 예외 분기에서 누락되기 쉽습니다.

record 또는 Timer.Sample의 시작·중지를 사용해 한 호출을 한 번만 기록합니다.

src/main/java/board/metrics/ImportTimer.java
package board.metrics;

import java.util.function.Supplier;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

public final class ImportTimer {
    private final MeterRegistry registry;

    public ImportTimer(MeterRegistry registry) {
        this.registry = registry;
    }

    public <T> T record(String format, Supplier<T> action) {
        var sample = Timer.start(registry);
        String outcome = "failure";
        try {
            T result = action.get();
            outcome = "success";
            return result;
        } finally {
            Timer timer = Timer.builder("board.import.duration")
                    .description("Post import latency")
                    .tag("format", format)
                    .tag("outcome", outcome)
                    .register(registry);
            sample.stop(timer);
        }
    }
}

형식은 markdown|json처럼 제한된 열거형에서 가져옵니다.

파일 이름을 태그로 쓰지 않습니다.

재시도 전체 지연 시간과 시도별 지연 시간은 이름 또는 태그 계약을 분리합니다.


SimpleMeterRegistry 검증

src/test/java/board/metrics/EntryMetricsTest.java
package board.metrics;

import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class EntryMetricsTest {
    @Test
    void recordsBoundedOutcomes() {
        var registry = new SimpleMeterRegistry();
        var metrics = new EntryMetrics(registry);

        metrics.accepted();
        metrics.accepted();
        metrics.rejected();

        assertThat(registry.get("board.entry.processed")
                .tag("outcome", "accepted").counter().count()).isEqualTo(2.0);
        assertThat(registry.get("board.entry.processed")
                .tag("outcome", "rejected").counter().count()).isEqualTo(1.0);
        assertThat(registry.getMeters()).hasSize(2);
    }
}

개수만 확인하면 예상치 못한 태그로 미터가 하나 더 생긴 회귀를 놓칩니다.

미터 수와 태그 키 집합, 설명도 검증합니다.

이 테스트의 관찰 결과는 accepted=2.0, rejected=1.0, registry meter count=2입니다.

이 테스트처럼 이 두 미터만 등록한 레지스트리에서 미터가 3개 이상이면 기대한 ID 집합과 실제 태그 조합을 대조합니다. accepted()를 두 번 호출한 이 코드에서 승인 건수가 1이면 호출·기록 경로를 확인해야 합니다. 이 단위 예제에는 데이터베이스 트랜잭션이나 커밋 분기가 없으므로 이 결과만으로 커밋 누락을 판정하지 않습니다.

측정값과 미터 ID의 카디널리티를 함께 확인합니다.

SimpleMeterRegistry 실행 결과
board.entry.processed{outcome=accepted}=2.0
board.entry.processed{outcome=rejected}=1.0
registered meters=2
result=METER_ID_CONTRACT_PRESERVED

연습 문제

회원 게시판 가져오기의 허용·거부 카운터와 형식·결과 타이머를 설계하세요.

영역·인스턴스 공통 태그를 포함한 최대 시계열 수를 계산하고 사용자 ID 태그를 등록하려 하면 거부하는 정책을 만드세요.

SimpleMeterRegistry에서 개수, 타이머 개수, 미터 ID 집합을 검증하세요.

해설 보기

메트릭 이름부터 짓지 말고 먼저 관측할 이벤트와 가능한 차원 집합을 표로 고정합니다.

백엔드 이름 변환은 레지스트리에 맡깁니다.

package board.metrics;

import java.util.Set;

public record TagContract(String meter, Set<String> allowedKeys) {
    public TagContract {
        allowedKeys = Set.copyOf(allowedKeys);
    }

    public void requireAllowed(String key) {
        if (!allowedKeys.contains(key)) {
            throw new IllegalArgumentException("UNBOUNDED_OR_UNKNOWN_TAG: " + key);
        }
    }
}

허용 키뿐 아니라 각 키의 최대 고유 값 수도 운영 중 측정합니다.