OpenTelemetry Collector spanmetrics로 샘플링 무관하게 정확한 RED 메트릭 구성하기
p99 지연시간이 200ms로 집계되는데 사용자 불만 티켓이 쏟아진다면, 샘플링 설정이 범인일 수 있습니다. 10% tail sampling 환경에서 에러율이 2%로 보인다면 실제 에러율은 얼마일까요? 정확히 알 수 없습니다. 샘플링된 트레이스는 전체 트래픽의 일부에 불과하고, 그 일부로 계산된 메트릭은 현실을 왜곡합니다.
OpenTelemetry Collector의 spanmetrics connector는 샘플링이 적용되기 전 단계에서 모든 스팬을 소비해 RED(Rate, Errors, Duration) 메트릭을 생성합니다. 트레이스는 비용 절감을 위해 10%만 저장하더라도, 메트릭은 100% 트래픽 기준으로 정확하게 측정됩니다. 이 글에서는 spanmetrics connector와 forward connector를 조합해 샘플링 비율에 관계없이 항상 정확한 RED 대시보드를 Prometheus + Grafana로 구성하는 전체 파이프라인을 단계별로 살펴봅니다.
이 글은 OpenTelemetry Collector를 이미 파이프라인에서 사용 중이고, Prometheus와 Grafana 환경이 갖춰진 독자를 대상으로 합니다. OTel Collector를 처음 접한다면 공식 Getting Started 문서를 먼저 읽어보시는 것을 권장합니다. 카디널리티 폭발 방지, PromQL 쿼리 작성, 실무에서 자주 발생하는 실수도 함께 다룹니다.
핵심 개념
OTel Collector의 컴포넌트 구조
OTel Collector는 네 종류의 컴포넌트로 구성됩니다. 전체 구조를 파악해야 파이프라인 설정이 자연스럽게 읽힙니다.
[Receiver] → [Processor] → [Exporter]
↕
[Connector] ← 두 파이프라인을 연결
↕
[Receiver] → [Processor] → [Exporter]| 컴포넌트 | 역할 |
|---|---|
| Receiver | 외부에서 데이터를 수신 (OTLP, Jaeger 등) |
| Processor | 데이터 가공·필터링 (transform, tail_sampling 등) |
| Exporter | 외부 시스템으로 데이터 전송 (Prometheus, Tempo 등) |
| Connector | 한 파이프라인의 exporter이자 다른 파이프라인의 receiver |
service.pipelines 블록에서 이 컴포넌트들을 조합해 데이터 흐름을 정의합니다.
spanmetrics connector의 이중 역할
커넥터(connector) 는 한 파이프라인의 exporter이자 다른 파이프라인의 receiver로 동작하는 컴포넌트입니다. 두 파이프라인을 연결하는 다리 역할을 합니다.
spanmetrics connector는 이 이중 역할로 트레이스 데이터를 메트릭으로 변환합니다.
[traces 파이프라인]
otlp receiver → spanmetrics connector (exporter 역할)
↓ 스팬 소비 → RED 메트릭 생성
[metrics 파이프라인]
spanmetrics connector (receiver 역할) → prometheus exporter설정 파일의 connectors.spanmetrics.namespace: traces가 메트릭 이름 앞에 traces_ 접두사를 붙입니다. exporters.prometheus.namespace: ""는 Prometheus exporter가 추가 접두사를 붙이지 않도록 비워둡니다. 결과적으로 최종 메트릭명은 다음과 같습니다.
| 메트릭명 | 타입 | 용도 |
|---|---|---|
traces_spanmetrics_calls_total |
Counter | Rate, Error 계산 |
traces_spanmetrics_duration_milliseconds_bucket |
Histogram | Duration(p99 등) 계산 |
target_info |
Gauge | 리소스 속성 메타데이터 |
tail sampling이란?
tail sampling은 트레이스 전체가 완성된 후 저장 여부를 결정하는 방식입니다. head sampling(수신 시점에 즉시 결정)과 달리, 에러 포함 트레이스나 느린 트레이스를 선별 보존할 수 있습니다. 단, 결정 대기 시간(
decision_wait) 동안 스팬을 메모리에 버퍼링하므로 메모리 사용량이 증가합니다.
샘플링과 메트릭의 관계: 파이프라인 순서가 핵심
[잘못된 순서]
otlp → tail_sampling(10%) → spanmetrics → prometheus
↑ 여기서 90% 버려짐 → 메트릭도 10% 기반으로 생성됨
[올바른 순서]
otlp → spanmetrics → 메트릭 100% 기반으로 생성
↘ forward → tail_sampling(10%) → 트레이스 백엔드핵심 원칙: spanmetrics connector는 반드시 샘플링 프로세서보다 앞 단계에 위치해야 합니다. tail sampling은 상태 유지 프로세서라 같은 파이프라인 내에서 분기가 불가능합니다.
forward connector를 사용해 파이프라인을 두 개로 분리하면 이 순서를 명확하게 강제할 수 있습니다.
도입 전 트레이드오프 요약
| 항목 | 내용 |
|---|---|
| 얻는 것 | 샘플링 비율에 무관한 정확한 RED 메트릭 |
| 잃는 것 | 메모리 사용량 증가, 수평 확장 복잡도 상승 |
| 필수 전제 | span name 정규화(카디널리티 관리), 단일 Collector 인스턴스 또는 Gateway 구조 |
실전 적용
개념을 이해했으니, 이제 실제 설정을 단계별로 살펴보겠습니다.
구성 단계 1: OTel Collector 전체 파이프라인 설정
아래는 forward connector를 활용해 100% 스팬에서 메트릭을 생성한 뒤, 별도 파이프라인에서 tail sampling을 적용하는 완전한 설정입니다. transform 프로세서를 통한 span name 정규화까지 포함한 최종 통합 설정입니다.
이 예시는 Docker Compose 환경 기준입니다.
tempo:4317은 Tempo 컨테이너의 서비스명을 사용합니다.
# otel-collector-config.yaml
connectors:
spanmetrics:
histogram:
explicit:
# Go duration 문자열 형식으로 지정합니다
buckets: ["5ms", "10ms", "25ms", "50ms", "100ms", "250ms", "500ms", "1s", "2.5s", "5s", "10s"]
dimensions:
- name: http.method
- name: http.status_code
- name: http.route
- name: service.name
metrics_flush_interval: 15s
# 이 namespace가 메트릭명 앞에 "traces_" 접두사를 붙입니다
# 최종 메트릭명: traces_spanmetrics_calls_total
namespace: traces
forward: {} # tail sampling 이후 트레이스를 백엔드로 전달
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
# 비워두면 prometheus exporter가 추가 접두사를 붙이지 않습니다
# (spanmetrics namespace "traces"와 충돌하지 않음)
namespace: ""
otlp/backend:
endpoint: tempo:4317 # Docker Compose 환경 기준
processors:
# span name의 가변 경로를 정규화해 카디널리티 폭발을 방지합니다
# spanmetrics보다 반드시 앞에 위치해야 합니다
transform:
trace_statements:
- context: span
statements:
- replace_pattern(name, "^/users/\\d+", "/users/{id}")
- replace_pattern(name, "^/orders/[a-f0-9\\-]{36}", "/orders/{id}")
tail_sampling:
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-traces-policy
type: latency
latency: { threshold_ms: 1000 }
- name: probabilistic-policy
type: probabilistic
probabilistic: { sampling_percentage: 10 }
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
service:
pipelines:
# 1단계: 모든 스팬 수신 → span name 정규화 → 메트릭 생성 + forward 전달
traces/all:
receivers: [otlp]
processors: [transform] # spanmetrics보다 앞에 위치
exporters: [spanmetrics, forward]
# 2단계: forward에서 받아 tail sampling 후 백엔드 전송
traces/sampled:
receivers: [forward]
processors: [tail_sampling]
exporters: [otlp/backend]
# 메트릭 파이프라인: spanmetrics가 생성한 메트릭 → Prometheus
metrics:
receivers: [spanmetrics]
exporters: [prometheus]각 설정 블록의 역할을 정리하면 다음과 같습니다.
| 설정 항목 | 역할 |
|---|---|
traces/all |
모든 스팬을 수신해 100% 기준 메트릭 생성 트리거 |
transform processor |
spanmetrics 전에 span name 정규화 → 카디널리티 제어 |
spanmetrics connector |
스팬 소비 → RED 메트릭 변환 |
forward connector |
메트릭 생성 이후 스팬을 다음 파이프라인으로 전달 |
traces/sampled |
tail sampling 적용 후 저장 비용 절감 |
metrics |
생성된 메트릭을 Prometheus로 노출 |
구성 단계 2: Prometheus 스크레이프 설정
OTel Collector가 0.0.0.0:8889에 메트릭을 노출하면, Prometheus가 주기적으로 수집합니다.
# prometheus.yml
scrape_configs:
- job_name: 'otel-spanmetrics'
scrape_interval: 15s
static_configs:
- targets: ['otel-collector:8889']scrape_interval은 metrics_flush_interval과 동일하거나 더 길게 설정하는 것을 권장합니다. spanmetrics connector가 15초마다 메트릭을 갱신하는데 Prometheus가 5초마다 수집한다면 동일한 값을 중복 수집하게 됩니다.
구성 단계 3: Grafana RED 대시보드 PromQL 쿼리
아래 세 쿼리가 RED 대시보드의 핵심입니다.
Rate와 Error Rate 쿼리의 span_kind="SPAN_KIND_SERVER" 필터는 서버가 직접 처리한 요청만 집계합니다. 서비스 A가 서비스 B를 호출하면, A는 CLIENT 스팬을, B는 SERVER 스팬을 동시에 생성합니다. 필터 없이 집계하면 동일한 요청이 두 번 카운트됩니다.
# Rate: 서비스별 초당 요청 수 (SERVER 스팬만 집계해 중복 방지)
sum(
rate(traces_spanmetrics_calls_total{span_kind="SPAN_KIND_SERVER"}[5m])
) by (service_name)# Error Rate: 서비스별 에러 비율 (%)
# 분자: SERVER 스팬 중 에러
# 분모: 모든 span_kind 대비 비율 → 전체 요청 대비 에러율로 해석됩니다
# span_kind 일관성이 필요하다면 분모에도 동일 필터를 추가할 수 있습니다
sum(
rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[5m])
) by (service_name)
/
sum(
rate(traces_spanmetrics_calls_total[5m])
) by (service_name)
* 100# Duration: 서비스별 p99 지연시간 (ms)
histogram_quantile(
0.99,
sum(
rate(traces_spanmetrics_duration_milliseconds_bucket[5m])
) by (service_name, le)
)장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 샘플링 독립적 | 샘플링 전 100% 스팬에서 메트릭 생성 — 수치 정확도 완전 보장 |
| 벤더 중립 | Prometheus, Grafana 등 오픈소스 스택과 완전 호환 |
| 추가 계측 불필요 | 기존 트레이스 계측 코드 수정 없이 RED 메트릭 자동 생성 |
| 세밀한 차원 설정 | dimensions로 Prometheus 레이블 조합을 명시적 제어 가능 |
| 히스토그램 버킷 커스터마이징 | SLO 기준에 맞는 버킷 정의로 정밀한 지연시간 분석 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 카디널리티 폭발 | span name에 가변 값 포함 시 시계열 수 폭발 | transform 프로세서로 span name 정규화 |
| 상태 유지 컴포넌트 | 동일 service.name 스팬은 같은 Collector 인스턴스에서 처리해야 집계 정확도 유지 |
Agent → Gateway 2계층 구조로 라우팅 |
| 스케일링 복잡성 | 수평 확장 시 Collector 2계층 배포 필요 | loadbalancingexporter + Gateway Collector 사용 |
| 메모리 사용량 | 집계 상태 + tail sampling 버퍼 → 고트래픽에서 메모리 압박 | metrics_flush_interval 단축, 불필요한 dimension 제거 |
| flush 지연 | metrics_flush_interval 설정에 따라 수 초 반영 지연 발생 |
실시간성 요구가 높은 경우 인터벌 값 조정 |
tail sampling 병행 사용 시 메모리 주의: tail sampling은
decision_wait동안 스팬을 메모리에 버퍼링합니다. spanmetrics와 동시에 사용하면 집계 상태와 트레이스 버퍼가 함께 메모리를 차지해 사용량이 크게 증가할 수 있습니다.
실무에서 가장 흔한 실수
-
spanmetrics를 tail_sampling 뒤에 배치하는 실수 — 메트릭이 샘플링된 트래픽 기준으로 생성되어 정확도가 크게 떨어집니다.
traces/all파이프라인에서 샘플링 프로세서 없이 spanmetrics를 먼저 내보내야 합니다. -
span name 정규화를 생략하는 실수 — REST API 경로에 리소스 ID가 포함된 서비스에서 자주 발생합니다. 프로덕션 투입 전 반드시
transform프로세서로 가변 경로를 패턴으로 치환하는 것을 권장합니다.transform은traces/all파이프라인에서 spanmetrics exporter보다 앞 단계 프로세서로 등록해야 합니다. -
Collector를 수평 확장할 때 상태 분산 문제를 간과하는 실수 — 동일 서비스의 스팬이 서로 다른 Collector 인스턴스에 분산되면 집계가 쪼개집니다.
loadbalancingexporter로service.name기준 해시 라우팅을 설정해 동일 서비스의 스팬이 항상 같은 Gateway Collector로 전달되도록 하는 것을 권장합니다.
마치며
spanmetrics connector와 forward connector를 조합하면, 샘플링 비율에 관계없이 항상 100% 트래픽 기반의 정확한 RED 메트릭을 Prometheus에서 수집하고 Grafana로 시각화할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
-
OTel Collector Contrib 설치 및 설정 작성:
docker run otel/opentelemetry-collector-contrib:latest이미지를 활용해 이 글의otel-collector-config.yaml을 기반으로spanmetrics,forward,tail_sampling블록을 구성해볼 수 있습니다. 설정 적용 후curl localhost:8889/metrics | grep traces_spanmetrics로 메트릭이 수집되는지 확인해보시는 것을 권장합니다. -
Prometheus 스크레이프 설정 추가:
prometheus.yml에job_name: 'otel-spanmetrics',targets: ['otel-collector:8889']를 추가하고 Prometheus를 재시작해traces_spanmetrics_calls_total메트릭이 수집되는지 확인해볼 수 있습니다. -
Grafana 대시보드 패널 추가: 이 글의 Rate, Error Rate, Duration PromQL 쿼리 세 개를 각각 Time series 패널로 추가하면 기본 RED 대시보드가 완성됩니다. 이후
http.routedimension을by절에 추가(by (service_name, http_route))해 엔드포인트별로 드릴다운하는 방향으로 발전시켜 나갈 수 있습니다.
Collector를 단일 인스턴스로 운영하는 동안은 이 구성으로 충분합니다. 트래픽이 늘어 수평 확장이 필요해지는 시점이 오면, Agent-Gateway 2계층 구조와 loadbalancingexporter를 도입해 tail sampling 집계 정확도를 유지하는 방법을 살펴보겠습니다.
다음 글: Collector를 수평 확장할 때 필요한 Agent-Gateway 2계층 구조와
loadbalancingexporter로 tail sampling 집계 정확도를 유지하는 방법
참고 자료
- spanmetrics connector README | opentelemetry-collector-contrib
- How to Use the Span Metrics Connector to Generate RED Metrics from Trace Data | OneUptime
- How to Build a Grafana RED Metrics Dashboard from OpenTelemetry Span Metrics | OneUptime
- Convert OpenTelemetry Traces to Metrics with SpanMetrics | Last9
- otelcol.connector.spanmetrics | Grafana Alloy documentation
- Tail sampling | Grafana OpenTelemetry documentation
- Scale Alloy tail sampling | Grafana OpenTelemetry documentation
- Span Metrics connector | Splunk Observability Cloud
- Metrics from traces | Grafana Tempo documentation
- Converting Traces to Metrics Using OTel Collector for Grafana Dashboards | nsalexamy
- Connectors | Red Hat build of OpenTelemetry 3.8