관측성(Observability) 3요소: 메트릭·로그·트레이스 제대로 이해하기
시스템이 복잡해질수록 “지금 무슨 일이 벌어지고 있나?”라는 질문에 답하기가 어려워집니다. 단일 프로세스 시절에는 로그 몇 줄로 충분했지만, 마이크로서비스가 수십 개 얽히고 쿠버네티스 위에서 수백 개 Pod가 떠 있다면 이야기가 달라집니다. 관측성(Observability)은 시스템의 외부 출력만으로 내부 상태를 추론할 수 있는 능력을 의미하며, 이를 실현하는 세 가지 핵심 도구가 메트릭(Metrics), 로그(Logs), 트레이스(Traces)입니다. 이 세 요소를 제대로 이해하고 적재적소에 활용하면 장애 탐지 시간(MTTD)과 복구 시간(MTTR)을 크게 줄일 수 있습니다.
1. 관측성 3요소 한눈에 비교
| 구분 | 메트릭 | 로그 | 트레이스 |
|---|---|---|---|
| 정의 | 시간 기반 수치 집계 | 이벤트의 구조화/비구조화 텍스트 기록 | 요청의 전체 실행 경로 추적 |
| 형식 | 타임스탬프 + 숫자값 + 레이블 | 타임스탬프 + 텍스트 (구조화 JSON 권장) | Trace ID + Span 트리 구조 |
| 질문 유형 | “지금 시스템이 얼마나 바쁜가?” | “무슨 일이 일어났나?” | “요청이 어디서 느려졌나?” |
| 스토리지 비용 | 낮음 (집계 압축 가능) | 높음 (원문 보존) | 중간 (샘플링으로 조절) |
| 주요 도구 | Prometheus, Datadog, CloudWatch | Loki, Elasticsearch, CloudWatch Logs | Jaeger, Zipkin, Tempo, OTLP |
| 주요 약점 | 카디널리티 폭발 위험 | 대용량 비용, 비구조화 파싱 복잡 | 오버헤드, 샘플링 누락 |
2. 메트릭: 숫자로 상태를 읽는다
메트릭은 시간 기반 수치 데이터입니다. CPU 사용률, HTTP 요청 수, 에러율, P99 레이턴시처럼 집계 가능한 값들이 해당됩니다. 수치를 시계열로 저장하기 때문에 데이터 크기가 작고 장기 보관이 쉬우며 알람(Alert)을 걸기에 가장 적합합니다.
메트릭을 설계할 때 가장 흔한 실수가 카디널리티(Cardinality) 폭발입니다. 카디널리티는 레이블 조합의 총 수를 의미합니다. 예를 들어 user_id를 레이블로 추가하면 사용자 수만큼 시계열이 생성되어 Prometheus 같은 저장소가 수백만 개의 시계열을 관리해야 합니다. 이는 메모리 과다 사용과 쿼리 성능 저하로 직결됩니다.
# 나쁜 예: user_id 레이블은 카디널리티를 폭발시킨다
http_requests_total{method="GET", path="/api/data", user_id="u-123456"}
# 좋은 예: 집계 가능한 저카디널리티 레이블만 사용
http_requests_total{method="GET", path="/api/data", status="200"}
# Prometheus alerting rule 예시
groups:
- name: api_alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "5xx 에러율 5% 초과"
레이블 설계 원칙: 레이블 값은 유한하고 예측 가능한 집합이어야 합니다. method(GET/POST/…), status(2xx/4xx/5xx), region(us-east-1/ap-northeast-2), service_name 정도가 안전한 레이블입니다.
3. 로그: 이벤트의 원문을 남긴다
로그는 시스템이 처리한 이벤트를 시간 순서대로 기록한 텍스트입니다. “에러가 났는데 왜 났나?”를 파악하는 데 가장 직접적인 단서를 제공합니다. 최근에는 구조화 로그(Structured Logging)가 표준으로 자리 잡았습니다. JSON 형태로 필드를 고정하면 Loki, Elasticsearch 등에서 필드 기반 검색과 집계가 쉬워집니다.
# Python 구조화 로그 (structlog 사용)
import structlog, logging
logging.basicConfig(format="%(message)s", level=logging.INFO)
log = structlog.get_logger()
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.processors.JSONRenderer(),
]
)
log.info(
"order_processed",
order_id="ORD-9912",
user_id="u-5523", # 로그에는 OK, 메트릭 레이블로는 NG
amount=49900,
duration_ms=87,
)
# 출력: {"timestamp": "2026-07-25T09:00:00Z", "level": "info",
# "event": "order_processed", "order_id": "ORD-9912", ...}
Loki + Promtail 로그 수집 설정
# promtail-config.yaml
server:
http_listen_port: 9080
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- json:
expressions:
level: level
order_id: order_id
- labels:
level:
order_id: # 주의: order_id는 카디널리티 높음. 검색용으로만 허용
- output:
source: message
로그 보관 정책도 중요합니다. 보안 규정 준수 목적의 감사 로그는 1년 이상 보관이 필요하지만, 디버그 수준 로그는 3~7일이면 충분합니다. 로그 레벨(DEBUG/INFO/WARN/ERROR)을 런타임에 동적으로 조절할 수 있는 구조를 갖추면 문제 발생 시 일시적으로 상세 로그를 활성화해 빠른 진단이 가능합니다.
4. 트레이스: 요청의 여정을 따라간다
분산 추적(Distributed Tracing)은 하나의 요청이 여러 서비스를 거치는 전체 경로를 단일 맥락으로 연결합니다. 각 서비스 호출이 하나의 Span이 되고, 동일한 Trace ID로 묶인 Span들이 트리를 이룹니다. “API 응답이 3초인데 어느 서비스에서 2.7초를 썼나?”라는 질문은 메트릭과 로그만으로는 찾기 어렵고 트레이스가 결정적 단서를 제공합니다.
# OpenTelemetry Python SDK - 수동 계측 예시
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Tracer 초기화
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("order-service")
def process_order(order_id: str):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("order.service", "order-svc")
# 하위 작업도 span으로 감싸면 Waterfall 뷰에서 분리 확인 가능
with tracer.start_as_current_span("validate_inventory"):
result = check_inventory(order_id)
span.set_attribute("inventory.available", result)
with tracer.start_as_current_span("charge_payment"):
charge(order_id)
return {"status": "ok"}
샘플링 전략
모든 요청을 100% 트레이싱하면 오버헤드와 저장 비용이 급증합니다. Head-based sampling(요청 시작 시 확률로 결정)은 구현이 쉽지만 중요한 에러 케이스를 놓칠 수 있습니다. Tail-based sampling(응답 완료 후 에러/고지연 기준으로 결정)은 정확하지만 구현이 복잡합니다. 실무에서는 정상 트래픽 1~5% 헤드 샘플링 + 에러/고지연(P99 초과) 100% 강제 수집의 조합이 효과적입니다.
5. OpenTelemetry: 단일 표준으로 통합
OpenTelemetry(OTel)는 CNCF가 관리하는 오픈소스 관측성 프레임워크로, 메트릭·로그·트레이스를 단일 SDK와 프로토콜(OTLP)로 수집합니다. 벤더 락인 없이 Datadog, Grafana Cloud, Jaeger, Prometheus 등 다양한 백엔드로 라우팅할 수 있는 것이 최대 장점입니다.
# OTel Collector 설정 예시 (otelcol-config.yaml)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
resource:
attributes:
- key: environment
value: production
action: upsert
exporters:
prometheus:
endpoint: "0.0.0.0:8889" # 메트릭 → Prometheus
loki:
endpoint: http://loki:3100/loki/api/v1/push # 로그 → Loki
otlp/jaeger:
endpoint: jaeger:4317 # 트레이스 → Jaeger
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch, resource]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [batch]
exporters: [loki]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger]
실전 팁: 언제 무엇을 먼저 보는가
- 알람이 울렸다 → 메트릭 대시보드로 어느 서비스/지역에서 에러율이 올라갔는지 먼저 확인합니다.
- 에러율의 원인을 찾는다 → 해당 시간대 + 해당 서비스의 에러 로그를 필터링해 스택 트레이스와 에러 메시지를 확인합니다.
- 느린 요청을 찾는다 → 트레이스에서 고지연 요청을 찾아 Waterfall 뷰로 어느 Span에서 시간이 소비됐는지 분석합니다.
- 카디널리티 관리: 메트릭 레이블은 100 이하 유한 집합으로 유지하고, 고카디널리티 데이터(요청 ID, 사용자 ID)는 로그 및 트레이스 속성으로 넣습니다.
- Exemplar 연결: Prometheus Exemplar를 활용하면 메트릭의 특정 데이터 포인트에서 직접 연관 트레이스 ID로 점프할 수 있어 메트릭-트레이스 간 맥락 전환이 빨라집니다.
- SLO 정의: 메트릭 기반 SLI(Service Level Indicator)를 설정하고 SLO(예: 가용성 99.9%, P99 레이턴시 500ms 이하)를 코드로 관리하면 관측성 데이터가 비즈니스 목표와 연결됩니다.
마무리
메트릭은 “무엇이 잘못됐나”를, 로그는 “왜 잘못됐나”를, 트레이스는 “어디서 잘못됐나”를 답해줍니다. 세 요소는 서로 대체 관계가 아니라 보완 관계입니다. OpenTelemetry를 통해 단일 계측 코드로 세 가지를 동시에 수집하고, 각 백엔드 도구를 목적에 맞게 배치하는 것이 현대 관측성 구축의 정석입니다. 카디널리티 관리와 샘플링 전략을 초반부터 설계에 반영하면 비용 폭발 없이 지속 가능한 관측성 스택을 운영할 수 있습니다.
자주 묻는 질문
Q. 메트릭, 로그, 트레이스를 모두 구축하려면 어디서 시작해야 하나요?
A. 우선순위는 메트릭 → 로그 → 트레이스 순입니다. 메트릭은 시스템 상태를 빠르게 파악하고 알람을 보내는 데 핵심이며 구축 비용이 낮습니다. Prometheus + Grafana 조합으로 핵심 서비스 메트릭부터 시작하고, 구조화 로그를 추가한 뒤, 마지막으로 OpenTelemetry SDK로 트레이스를 계측하는 단계적 접근을 권장합니다.
Q. Prometheus 메트릭이 메모리를 너무 많이 사용합니다. 어떻게 줄이나요?
A. 대부분 카디널리티 폭발이 원인입니다. prometheus_tsdb_head_series 메트릭으로 활성 시계열 수를 확인하고, topk(20, count by (__name__)({job="your-service"})) 쿼리로 가장 많은 시계열을 생성하는 메트릭을 찾으세요. 해당 메트릭의 레이블을 검토해 고카디널리티 레이블을 제거하거나, recording rule로 미리 집계해 원시 시계열을 줄이는 것이 효과적입니다.
Q. 분산 추적에서 Trace ID가 서비스 간에 전파되지 않습니다. 어떻게 해야 하나요?
A. W3C TraceContext 헤더(traceparent, tracestate)가 HTTP 요청에 포함되지 않거나 서비스가 이를 파싱하지 않는 경우입니다. OpenTelemetry SDK를 사용하면 자동 계측(Auto-Instrumentation)으로 대부분의 HTTP/gRPC 클라이언트에서 헤더 전파를 자동 처리합니다. 커스텀 HTTP 클라이언트를 사용한다면 propagate.inject(headers)와 propagate.extract(headers)를 명시적으로 호출해 컨텍스트를 전달해야 합니다.