Blog
prometheusmetricsmonitoringobservabilitypromql

Prometheus 메트릭 제대로 이해하기 — metric name, label, instance/job, 그리고 gauge vs counter

Prometheus 메트릭이 헷갈리는 이유를 하나씩 정리합니다. 메트릭 한 줄의 구조, 이름 짓기 규칙, label과 카디널리티, instance·job이 어디서 오는지, counter와 gauge의 차이, 그리고 클라이언트 라이브러리로 value를 지정하는 방법까지 코드와 함께 다룹니다.

Data Dynamics2026년 6월 12일14 min read

Prometheus 를 처음 다루면 비슷한 지점에서 막힙니다. "이 메트릭 이름은 왜 _total 로 끝나지?", "instancejob 라벨은 내가 만든 적도 없는데 어디서 나온 거지?", "countergauge 는 뭐가 다르고, 값은 어떻게 넣는 거지?"

이 글은 그 혼란을 구조부터 차근차근 풀어냅니다. 메트릭 한 줄이 어떻게 생겼는지에서 시작해, 이름·라벨 규칙, instance/job 의 정체, 메트릭 타입의 차이, 그리고 실제 코드로 값을 지정하는 방법까지 정리합니다.

1. 메트릭 한 줄의 구조

Prometheus 가 수집하는 데이터는 결국 텍스트 한 줄입니다. 익스포터(exporter)가 /metrics 엔드포인트로 아래 같은 형식(exposition format)을 노출하면, Prometheus 가 주기적으로 긁어(scrape) 갑니다.

# HELP http_requests_total 처리한 HTTP 요청의 누적 개수
# TYPE http_requests_total counter
http_requests_total{method="POST",code="200"} 1027
http_requests_total{method="POST",code="400"} 3

마지막 데이터 줄을 분해하면 이렇게 됩니다.

http_requests_total   {method="POST",code="200"}   1027   [timestamp]
└── metric name ──┘   └──────── labels ────────┘   value   (선택, 보통 생략)
  • metric name — 무엇을 측정하는가 (http_requests_total)
  • labels — 어떤 차원으로 쪼개는가 (method, code)
  • value — 현재 숫자 값 (float64). 타임스탬프는 보통 생략하고, Prometheus 가 scrape 한 시각을 붙입니다.

# HELP 는 사람이 읽는 설명, # TYPE 은 메트릭 타입 선언입니다. 이 둘은 메트릭마다 한 번씩 나옵니다.

핵심: metric_name{label="value"} value 하나가 하나의 시계열(time series) 입니다. 라벨 조합이 달라지면 전부 별개의 시계열이 됩니다 — 이 사실이 뒤에 나올 카디널리티 이야기의 출발점입니다.

2. metric name — 이름 짓기 규칙

이름은 자유롭게 지을 수 있지만, 관례를 따르면 PromQL 질의와 대시보드가 훨씬 깔끔해집니다.

  • snake_case, ASCII 영문·숫자·언더스코어만. (콜론 : 은 사용자 메트릭에 쓰지 마세요 — 레코딩 룰 전용입니다.)
  • 네임스페이스(접두사) 를 붙입니다: http_requests_total, process_cpu_seconds_total, node_memory_MemFree_bytes 처럼 애플리케이션/서브시스템을 앞에 둡니다.
  • 기본 단위(base unit) 를 씁니다. 밀리초가 아니라 (_seconds), 메가바이트가 아니라 바이트(_bytes), 비율은 0~1. 단위는 이름 끝에 붙입니다.
  • counter 는 _total 접미사. 예: app_errors_total.
  • _count, _sum, _bucket 은 histogram/summary 가 자동으로 만드는 예약 접미사이므로 직접 쓰지 마세요.

이름만 봐도 "무엇을, 어떤 단위로, 어떤 타입으로" 재는지 드러나는 것이 목표입니다. request_latency_seconds 는 좋고, latencyMs 는 나쁩니다.

3. label — 차원, 그리고 카디널리티 함정

라벨은 같은 메트릭을 여러 차원으로 쪼개는 key-value 입니다. http_requests_total 하나를 method, code, endpoint 로 나눠 보는 식이죠.

http_requests_total{method="GET",  endpoint="/api/users", code="200"} 8123
http_requests_total{method="POST", endpoint="/api/users", code="201"} 412
http_requests_total{method="GET",  endpoint="/api/orders",code="500"} 7

여기서 가장 흔한 사고가 카디널리티 폭발입니다. 라벨 값의 조합 하나하나가 별도 시계열이므로, 값의 가짓수가 무한히 늘어나는 라벨을 붙이면 Prometheus 메모리가 폭발합니다.

  • 나쁜 라벨: user_id, email, request_id, full_url(쿼리스트링 포함), 타임스탬프 — 값이 사실상 무한대
  • 좋은 라벨: method(GET/POST/…), status_code, region, endpoint(템플릿화된 경로) — 값의 가짓수가 유한하고 작음

경험칙: 한 메트릭의 모든 라벨 조합 수는 수천을 넘기지 않도록 설계하세요. "이 값이 며칠 뒤에도 유한할까?" 를 항상 자문합니다.

참고로 __ 로 시작하는 라벨(__name__, __address__ 등)은 Prometheus 내부 예약 라벨입니다. 사실 메트릭 이름 자체도 내부적으로는 __name__ 라벨입니다.

4. instance 와 job 은 어디서 오는가 (가장 헷갈리는 부분)

익스포터가 노출한 메트릭에는 instancejob 도 없었습니다. 그런데 Prometheus 에서 질의하면 이렇게 나옵니다.

http_requests_total{job="my-app", instance="10.0.0.1:8000", method="GET", code="200"} 8123

jobinstance익스포터가 아니라 Prometheus 가 scrape 시점에 자동으로 붙이는 "타깃 라벨" 입니다. 출처는 scrape 설정입니다.

# prometheus.yml
scrape_configs:
  - job_name: "my-app"                 # → 모든 메트릭에 job="my-app" 부여
    static_configs:
      - targets:
          - "10.0.0.1:8000"            # → instance="10.0.0.1:8000"
          - "10.0.0.2:8000"            # → instance="10.0.0.2:8000"
  • job — scrape 설정의 job_name 값이 그대로 라벨이 됩니다. "이 메트릭은 어떤 서비스 그룹에서 왔는가."
  • instance — 기본적으로 타깃 주소(host:port, 내부 라벨 __address__)가 들어갑니다. "그 그룹 안에서 정확히 어느 프로세스/머신인가."

그래서 익스포터 코드에는 instance/job 을 넣지 않습니다(넣으면 충돌). instance 를 호스트명 등으로 바꾸고 싶다면 relabeling 을 씁니다.

    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        replacement: "web-01"

그리고 Prometheus 는 타깃마다 up 이라는 메트릭을 자동 생성합니다 — scrape 성공이면 1, 실패면 0. 이것도 job/instance 라벨을 달고 나옵니다.

up{job="my-app", instance="10.0.0.1:8000"} 1

정리: metric name 과 method/code 같은 라벨은 "내가(익스포터)" 정한 것, job/instance/up 은 "Prometheus 가" 붙인 것. 이 경계만 잡으면 절반은 안 헷갈립니다.

5. 메트릭 타입 — counter vs gauge (vs histogram/summary)

값을 어떻게 해석할지가 타입으로 갈립니다. 가장 많이 쓰는 둘부터.

Counter — 단조 증가 (누적)

  • 오직 증가만 합니다(프로세스 재시작 시 0 으로 리셋). 절대값 자체는 의미가 약하고, 변화율이 중요합니다.
  • 예: http_requests_total, errors_total, bytes_sent_total
  • 질의는 거의 항상 rate() / increase() 로 감쌉니다. "지금 초당 몇 건?"

Gauge — 오르내리는 현재값

  • 올라가고 내려가는 스냅샷 값. 그 순간의 상태를 그대로 의미합니다.
  • 예: node_memory_MemAvailable_bytes, queue_length, temperature_celsius, inprogress_requests
  • 질의는 값을 그대로 보거나 avg_over_time(), max_over_time() 등으로 봅니다.

핵심 구분 질문: "이 숫자가 줄어들 수 있는가?" 줄어들 수 있으면 gauge, 누적되며 증가만 하면 counter 입니다. 큐 길이는 gauge, 큐에 들어온 누적 건수는 counter 입니다.

Histogram / Summary — 분포 (latency 등)

응답시간처럼 "분포"가 궁금할 때 씁니다. Histogram 은 관측값을 버킷(le, less-than-or-equal)에 누적하고, _bucket / _sum / _count 시계열을 자동 노출합니다. 분위수는 질의 시점에 histogram_quantile() 로 계산합니다.

# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.1"}  240
http_request_duration_seconds_bucket{le="0.5"}  290
http_request_duration_seconds_bucket{le="+Inf"} 300
http_request_duration_seconds_sum   48.2
http_request_duration_seconds_count 300

Summary 는 클라이언트가 분위수를 미리 계산해 노출합니다(집계가 어렵다는 단점). 대부분의 경우 서버 측 집계가 가능한 histogram 을 권장합니다.

6. value 는 어떻게 지정하는가 (클라이언트 라이브러리)

텍스트를 직접 출력할 수도 있지만, 보통은 공식 클라이언트 라이브러리로 메트릭 객체를 만들고 메서드를 호출합니다. 타입마다 허용되는 조작이 다릅니다 — 이게 처음에 헷갈리는 또 한 지점입니다.

from prometheus_client import Counter, Gauge, Histogram
 
# Counter — 증가만 가능 (.inc), set/dec 없음
REQUESTS = Counter(
    "app_requests_total", "처리한 요청 수", ["method", "endpoint"]
)
REQUESTS.labels(method="GET", endpoint="/api").inc()      # +1
REQUESTS.labels(method="GET", endpoint="/api").inc(3)     # +3
 
# Gauge — 자유롭게 set/inc/dec
INPROGRESS = Gauge("app_inprogress_requests", "처리 중인 요청 수")
INPROGRESS.set(0)
INPROGRESS.inc()        # +1
INPROGRESS.dec()        # -1
INPROGRESS.set(42)      # 절대값 지정
INPROGRESS.set_to_current_time()   # 현재 시각(unix ts)
 
# Histogram — 관측값을 넣음 (.observe)
LATENCY = Histogram(
    "app_request_duration_seconds", "요청 처리 시간",
    buckets=[0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
)
LATENCY.observe(0.23)

요점:

  • Counter.inc() / .inc(n) 만. 값을 직접 "세팅"하지 않습니다(누적이니까).
  • Gauge.set() / .inc() / .dec() 모두 됩니다. 상태값이니까요.
  • Histogram.observe(값) 으로 표본을 넣으면 버킷/합/개수가 알아서 갱신됩니다.

라벨이 있는 메트릭은 항상 .labels(...) 로 라벨 값을 먼저 고른 뒤 조작합니다. 시간 측정은 데코레이터/컨텍스트 매니저가 편합니다.

# 함수 실행 시간을 자동으로 observe
@LATENCY.time()
def handle():
    ...
 
# 또는 블록 단위
with LATENCY.time():
    do_work()
 
# Gauge 의 동시 처리량 추적
with INPROGRESS.track_inprogress():
    do_work()

7. 직접 노출해보기 (최소 익스포터)

위 메트릭들을 HTTP 로 노출하는 가장 작은 예제입니다. start_http_server/metrics 엔드포인트를 띄웁니다.

import random, time
from prometheus_client import start_http_server, Counter, Gauge
 
REQUESTS = Counter("app_requests_total", "요청 수", ["code"])
INPROGRESS = Gauge("app_inprogress_requests", "처리 중 요청")
 
if __name__ == "__main__":
    start_http_server(8000)          # http://localhost:8000/metrics
    while True:
        INPROGRESS.inc()
        time.sleep(random.random())
        REQUESTS.labels(code="200").inc()
        INPROGRESS.dec()

띄운 뒤 확인:

curl -s localhost:8000/metrics | grep app_

8000 포트를 앞서 본 scrape_configs 의 타깃으로 등록하면, Prometheus 가 긁어가면서 job/instance 를 붙여 저장합니다.

8. PromQL 로 읽기 — 타입별 질의

타입에 따라 읽는 방법이 다릅니다.

# Counter: 절대값이 아니라 초당 증가율을 본다
rate(app_requests_total[5m])
 
# endpoint 별로 합쳐서 보기
sum by (endpoint) (rate(app_requests_total[5m]))
 
# 에러율 = 5xx 비율
sum(rate(app_requests_total{code=~"5.."}[5m]))
  / sum(rate(app_requests_total[5m]))
 
# Gauge: 값을 그대로
app_inprogress_requests
 
# Histogram: 95 분위 응답시간
histogram_quantile(
  0.95,
  sum by (le) (rate(app_request_duration_seconds_bucket[5m]))
)

counter 에 rate() 를 안 쓰고 생값을 그래프로 그리면 "계속 우상향하는 톱니" 만 보입니다 — 거의 항상 rate()/increase() 가 정답입니다.

9. 자주 헷갈리는 것 정리

  • _total 은 왜 붙나? counter 의 관례 접미사입니다. "누적 총합"임을 드러냅니다.
  • instance/job 을 내 코드에서 넣어야 하나? 아니요. Prometheus 가 scrape 시 붙입니다.
  • counter 인데 값이 줄었어요. 프로세스 재시작으로 0 리셋된 것입니다. rate() 는 이 리셋을 자동 보정합니다.
  • gauge 에 rate() 를 써도 되나? 의미가 없습니다. gauge 는 생값/*_over_time() 으로 봅니다.
  • 단위는 왜 초·바이트인가? 생태계 표준입니다. ms·MB 로 노출하면 다른 대시보드·룰과 안 맞습니다.
  • 라벨에 ID 를 넣고 싶어요. 멈추세요. 카디널리티 폭발의 지름길입니다. 높은 카디널리티는 로그/트레이스의 몫입니다.

마무리

Prometheus 메트릭은 결국 metric_name{labels} value 한 줄로 환원됩니다. 여기에 (1) 이름·단위 관례, (2) 라벨과 카디널리티, (3) job/instance 는 Prometheus 가 붙인다는 사실, (4) counter/gauge/histogram 의 의미 차이, (5) 타입별 value 지정 방법 — 이 다섯 가지만 잡으면 처음의 혼란은 대부분 사라집니다.

다음 단계로는 같은 익스포터를 Grafana 대시보드에 연결하고, counter 기반 에러율·gauge 기반 포화도(saturation)·histogram 기반 지연 분위수를 한 화면에 묶어 보는 것을 권합니다. 메트릭을 "잘 짓는" 것이 결국 좋은 관측성의 8할입니다.