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
This post is not yet translated. The original Korean version is shown below.

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할입니다.