Prometheus·Grafana
Prometheus 레지스트리·Actuator 수집 엔드포인트·서비스 탐색·카운터 비율·타이머 히스토그램·Grafana 대시보드를 연결하고 @Counted·@Timed 적용 경계, 초기화·시계열 노후화 판정·레이블 카디널리티를 검증합니다.
애플리케이션 레지스트리에 값이 보인다고 대시보드가 자동으로 채워지지는 않습니다. Prometheus용 레지스트리가 미터를 Prometheus 형식의 텍스트로 만들고, Actuator 엔드포인트가 이를 노출하며, Prometheus 대상이 주기적으로 수집하고, Grafana가 시계열을 쿼리해야 합니다. 어느 단계가 끊겨도 패널은 비지만 원인과 복구 방법은 다릅니다.
Prometheus 수집 엔드포인트
Prometheus 레지스트리 의존성과 Actuator가 있으면 /actuator/prometheus 엔드포인트가 후보가 됩니다.
웹 노출 허용 목록, 관리 보안, 서비스 탐색 대상을 모두 통과해야 수집됩니다.
브라우저에서 한 번 200을 받은 것보다 Prometheus Targets 화면의 최근 수집, 지속 시간, 오류, up을 확인합니다.
management:
endpoints:
web:
exposure:
include: "health,prometheus"
scrape_configs:
- job_name: board
metrics_path: /actuator/prometheus
scrape_interval: 15s
static_configs:
- targets: ["board-management:9091"]운영 환경은 정적 대상보다 오케스트레이터의 서비스 탐색을 사용하고 URL 스킴, 인증, TLS, 레이블 재작성을 함께 구성합니다. 수집 간격을 15초로 두면 2초 동안 발생한 순간 급증이 평균이나 게이지 표본에서 사라질 수 있음을 이해합니다.
메트릭 이름·레이블 변환
Micrometer의 board.entry.processed 카운터에는 Prometheus에서 밑줄과 _total 관례가 적용될 수 있습니다.
동일 미터의 태그는 레이블이 됩니다.
대시보드 쿼리를 Java 이름만 보고 작성하지 말고 실제 노출 형식의 HELP, TYPE, 표본 이름과 레이블을 확인합니다.
# HELP board_entry_processed_total Completed entry processing attempts
# TYPE board_entry_processed_total counter
board_entry_processed_total{outcome="accepted"} 42.0
board_entry_processed_total{outcome="rejected"} 3.0레이블 값에 게시글 ID를 넣으면 수집 페이로드와 백엔드 시계열이 계속 증가합니다. 경로, 메서드, 정해진 결과 코드처럼 집계 가능한 레이블을 선택하고 원시 식별자는 트레이스나 로그로 보냅니다.
카운터 쿼리
누적 카운터 42만으로 최근 트래픽을 알 수 없습니다.
rate(...[5m])는 초당 평균 증가율, increase(...[1h])는 구간의 증가량을 묻습니다.
프로세스 재시작으로 값이 0으로 내려가는 초기화를 PromQL 함수가 처리하므로 게이지처럼 마지막 값끼리 단순 빼지 않습니다.
구간이 수집 간격보다 너무 짧으면 표본이 부족하고 그래프가 흔들립니다.
경보에는 즉시 반응과 안정성의 절충을 고려해 여러 구간을 사용합니다.
sum by (outcome)처럼 필요한 레이블만 유지하고 인스턴스를 합칠지 상세 분석할지 패널 목적에 맞춥니다.
package board.metrics;
public enum Outcome {
ACCEPTED("accepted"),
REJECTED("rejected"),
FAILED("failed");
private final String tagValue;
Outcome(String tagValue) {
this.tagValue = tagValue;
}
public String tagValue() {
return tagValue;
}
}열거형에서 레이블 값을 만들면 오타와 임의 예외 이름의 유입을 막습니다.
타이머 지연 분포
타이머 개수와 전체만 있으면 평균은 계산할 수 있지만 느린 소수 요청을 숨깁니다.
서비스 수준의 지연 시간을 판단하려면 히스토그램 버킷을 발행하고 histogram_quantile로 전체 인스턴스의 집계 백분위수를 계산하거나 SLO 경계 버킷 비율을 사용합니다.
클라이언트 측 백분위수는 인스턴스 간 합산이 어렵다는 점을 구별합니다.
버킷을 모든 URI와 상태 레이블에 무분별하게 켜면 시계열 수가 버킷 개수만큼 늘어납니다. 중요한 작업에만 SLO 경계를 설정하고 실제 트래픽 범위로 버킷을 조정합니다. 최댓값은 시간 구간에 따라 사라질 수 있으므로 영구적인 최악값으로 해석하지 않습니다.
@Counted·@Timed 프록시
애노테이션 계측은 업무 코드 침투를 줄이지만 애스펙트 빈, 프록시 적용 가능성, 포인트컷, 내부 호출 제약을 가집니다. 애노테이션 이름과 태그 규칙을 코드 검토하고 실패·예외 태그 값이 제한적인지 확인합니다.
package board.metrics;
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;
@Service
public class MeasuredImportService {
@Counted(value = "board.import.invocations", extraTags = {"format", "markdown"})
@Timed(value = "board.import.duration", extraTags = {"format", "markdown"})
public int importMarkdown(String body) {
if (body.isBlank()) {
throw new IllegalArgumentException("empty markdown");
}
return body.lines().mapToInt(line -> 1).sum();
}
}같은 이벤트에 수동 카운터와 애노테이션 카운터를 동시에 적용해 두 배 기록하지 않습니다.
자기 호출과 직접 new 테스트에서 애노테이션이 실행되지 않는 신호를 별도 런타임 테스트로 둡니다.
Grafana 패널 의미
“주문 수” 패널 대신 “최근 5분의 허용 비율은 기준선에서 벗어났는가”처럼 답하려는 질문을 제목으로 씁니다. 쿼리, 범례, 단위, 최솟값·최댓값, 임계값, 대응 절차 링크를 버전 관리합니다. 바이트를 십진 또는 이진 단위 중 무엇으로 표시하는지 정하고 지연 시간의 초를 밀리초처럼 잘못 표시하지 않습니다.
0과 데이터 없음은 다릅니다.
수집이 끊긴 패널을 0으로 채우면 트래픽이 사라진 장애를 정상 유휴로 오해합니다.
up, 표본 경과 시간, 애플리케이션 메트릭을 함께 배치하고 배포 애노테이션으로 변화 시점을 표시합니다.
수집 실패 진단
application registry has meter=false -> INSTRUMENTATION_MISSING
prometheus endpoint lacks sample -> EXPORT_FILTER_OR_NAMING
target up=0 -> SCRAPE_AUTH_NETWORK_FAILURE
Prometheus query returns series=true, panel empty=true -> GRAFANA_QUERY_OR_TIME_RANGE
sample age > 2 * scrape interval -> STALE_DATA대시보드만 새로고침하지 말고 애플리케이션 레지스트리부터 노출 형식, 대상, 원시 쿼리, 패널 변환 순으로 추적합니다.
연습 문제
회원 게시판 허용·거부 카운터와 가져오기 타이머를 Prometheus로 수집하세요.
최근 5분 비율, 1시간 increase, p95 지연 시간을 Grafana 패널로 만들고 인스턴스 초기화와 대상 중단을 재현하세요.
애노테이션 방식은 프록시 호출과 자기 호출에서 미터 개수가 어떻게 다른지 검증하세요.
해설 보기
패널을 만들기 전에 원시 PromQL 결과와 레이블 집합을 확인합니다. 카운터에는 초기화를 처리하는 함수를 사용하고, 타이머에는 여러 인스턴스를 집계할 수 있는 히스토그램을 사용합니다.
package board.metrics;
import java.time.Duration;
public record ScrapeFreshness(Duration interval, Duration age) {
public boolean stale() {
return age.compareTo(interval.multipliedBy(2)) > 0;
}
public String displayState() {
return stale() ? "NO_CURRENT_DATA" : "CURRENT_DATA";
}
}트래픽 0과 수집 중단을 같은 색으로 표시하지 않습니다.