Blog
prometheusmetricsmonitoringobservabilitypromql

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

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

Data Dynamics2026년 6월 12일17 min read

Prometheus UI 에서 처음으로 메트릭을 조회했을 때를 기억하시나요? "이 이름은 왜 _total 로 끝나지?", "instancejob 라벨은 내가 만든 적도 없는데 어디서 나온 거지?", "countergauge 는 뭐가 다르고, 값은 어떻게 넣는 거지?" 같은 질문이 연달아 떠오르면서 막히게 되죠.

이 글에서 배우는 것

  • 메트릭 한 줄의 구조와 시계열의 개념
  • 이름 짓기 규칙과 단위 관례
  • 라벨과 카디널리티 — 왜 높은 카디널리티가 위험한지
  • instance / job 이 어디서 오는지 (가장 많이 헷갈리는 부분)
  • counter / gauge / histogram 의 차이와 value 를 지정하는 방법

이 글은 그 혼란을 구조부터 차근차근 풀어냅니다. 메트릭 한 줄이 어떻게 생겼는지에서 시작해, 이름·라벨 규칙, 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 질의와 대시보드가 훨씬 깔끔해집니다. 나중에 여러분이나 동료가 latencyMs 라는 이름을 보고 "이게 밀리초야, 초야?" 하며 헷갈리는 사태를 막을 수 있거든요.

  • 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 은 어디서 오는가 (가장 헷갈리는 부분)

가장 많이 받는 질문입니다. "내가 코드에서 instance 를 만든 적이 없는데 왜 붙어 있지?" 익스포터가 노출한 메트릭에는 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)

값을 어떻게 해석할지가 타입으로 갈립니다. 타입을 잘못 고르면 PromQL 에서 엉뚱한 값이 나오거든요. 가장 많이 쓰는 둘부터 살펴봅시다.

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(...) 로 라벨 값을 먼저 고른 뒤 조작합니다. 시간 측정은 매번 time.perf_counter() 를 쓰지 않아도 됩니다 — 데코레이터/컨텍스트 매니저가 훨씬 편합니다.

# 함수 실행 시간을 자동으로 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 가 15초마다 긁어가면서 job/instance 를 붙여 저장합니다. 4장에서 설명한 그 과정이죠.

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

메트릭을 노출했으면 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할입니다.

마치며 — 핵심 요약

  • 메트릭 한 줄은 metric_name{labels} value 이고, 라벨 조합 하나하나가 별도 시계열입니다.
  • 이름에는 단위를 붙이세요 — 초(_seconds), 바이트(_bytes), counter 는 _total 접미사.
  • 라벨 값이 무한정 늘어날 수 있는 것(user_id, URL 등)은 절대 라벨로 쓰지 마세요 — 카디널리티 폭발의 원인입니다.
  • job / instance 는 익스포터가 아니라 Prometheus 가 scrape 시점에 붙입니다. 코드에서 넣으면 충돌납니다.
  • counter 는 오직 증가, gauge 는 오르내림, histogram 은 관측값 분포. 타입 선택이 PromQL 질의 방식을 결정합니다.
  • counter 를 그래프로 그릴 때는 반드시 rate() 또는 increase() 로 감싸세요.