OTel Collector(OpenTelemetry Collector)에서 filterprocessor · transformprocessor로 스팬 노이즈와 카디널리티 폭발 제어하기
헬스체크 엔드포인트 스팬이 전체 트레이스 볼륨의 30~40%를 차지하고 있다는 사실을 처음 마주했을 때, 많은 팀이 당혹스러움을 경험합니다. Kubernetes 환경에서는 /health, /readiness, /liveness에 대한 프로브 요청이 수초마다 쏟아지고, 이 스팬들이 Jaeger나 Grafana Tempo의 저장 용량을 조용히 잠식합니다. 여기에 user.id나 request.id처럼 요청마다 고유한 값을 담은 속성이 더해지면, 수백만 개의 유니크 시계열을 만들어내는 **카디널리티 폭발(Cardinality Explosion)**로 이어져 대시보드 쿼리가 느려지고 관측 가능성 비용이 기하급수적으로 증가합니다.
OpenTelemetry Collector(OTel Collector)는 이 두 문제를 동시에 해결할 수 있는 두 가지 강력한 프로세서를 제공합니다. filterprocessor로 가치 없는 스팬을 파이프라인 초입에서 영구 제거하고, transformprocessor로 고카디널리티를 유발하는 속성을 정제하면, 백엔드에 도달하는 데이터 품질과 비용을 동시에 통제할 수 있습니다.
이 글을 읽고 나면 두 프로세서를 조합해 스팬 노이즈를 제거하고 카디널리티를 낮추는 YAML 파이프라인 설정을 직접 작성할 수 있습니다. OTel Collector를 처음 접하시는 분을 위해 receivers와 exporters를 포함한 완전한 설정 파일 예시도 함께 살펴봅니다.
목차
핵심 개념
OTel Collector 파이프라인 구조
OTel Collector는 수신(Receiver) → 처리(Processor) → 내보내기(Exporter) 세 단계로 구성된 파이프라인에서 텔레메트리 데이터를 흐르게 합니다. 프로세서는 이 흐름의 중간에서 데이터를 가공하는 레이어로, Jaeger·Prometheus·Grafana Tempo 같은 백엔드에 도달하기 전 마지막 관문 역할을 합니다.
[앱 서비스] → Receiver(OTLP) → [filterprocessor] → [transformprocessor] → [batch] → Exporter프로세서는 스팬 단위로 작동합니다. 각 조건식은 개별 스팬의 속성을 평가하고, 조건을 충족한 스팬만 삭제하거나 변환합니다. 파이프라인의 순서는 service.pipelines 블록의 processors 배열 순서로 결정되며, 이 순서는 처리 결과에 직접 영향을 미칩니다.
filterprocessor: 조건 기반 영구 삭제
filterprocessor는 OTTL(OpenTelemetry Transformation Language) 조건식을 평가해 조건을 충족하는 텔레메트리를 파이프라인에서 즉시 제거합니다.
processors:
filter:
error_mode: ignore
traces:
span:
- attributes["http.route"] == "/health"
- attributes["http.route"] == "/readiness"
error_mode: ignore란? 스팬에 특정 속성이 없을 때 조건 평가에서 오류가 발생할 수 있는데,ignore로 설정하면 오류를 무시하고 파이프라인을 계속 진행합니다. 프로덕션 환경에서는 거의 항상 설정하는 것을 권장합니다.
OR 논리 vs AND 조합을 이해하는 것이 중요합니다. traces.span: 배열에 나열된 여러 조건은 OR 논리로 평가됩니다. 즉, 나열된 조건 중 하나라도 참이면 해당 스팬은 드롭됩니다. 반면 한 조건 내에서 and 키워드를 사용하면 두 조건이 모두 충족될 때만 드롭됩니다.
traces:
span:
- attributes["http.route"] == "/health" # 조건 A
- attributes["http.route"] == "/readiness" # 조건 B — A 또는 B이면 드롭 (OR)
- metric.name == "foo" and IsMatch(...) # 한 항목 안의 AND — 둘 다 참일 때만 드롭transformprocessor: 속성 변환·삭제·마스킹
transformprocessor는 OTTL 문(statement) 목록을 순서대로 실행해 속성 추가·삭제·값 치환·잘라내기 등 다양한 변환을 수행합니다. where 절로 조건부 실행도 지원합니다.
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
- delete_key(attributes, "user.id")
- truncate_all(attributes, 256)
- set(name, "normalized")
where IsMatch(name, "^/api/v[0-9]+/.*")OTTL(OpenTelemetry Transformation Language)이란? 두 프로세서가 공통으로 사용하는 쿼리·변환 언어로, SQL과 유사한 표현식 문법을 가집니다.
IsMatch(),delete_key(),truncate_all(),set(),Concat()등의 내장 함수를 제공합니다.
두 프로세서의 YAML 구조 차이:
filterprocessor는traces.span:배열에 OTTL 조건식을 직접 나열하는 방식을 사용하고,transformprocessor는trace_statements[].context: span+statements:구조를 사용합니다. filter는 조건 평가(evaluation)만 수행하고, transform은 실행 컨텍스트와 여러 문(statement)을 순서대로 처리하기 때문에 YAML 구조가 다르게 설계되어 있습니다. 처음 접하시면 두 프로세서의 YAML 모양이 달라 혼란스러울 수 있으니, 이 차이를 기억해두시면 좋습니다.
카디널리티(Cardinality)란?
스팬이나 메트릭의 속성 값이 가질 수 있는 유일한 조합의 수입니다. 예를 들어 http.route 속성에 /users/12345, /users/67890처럼 실제 사용자 ID가 포함되면, 각 고유 ID마다 새로운 시계열이 생성됩니다. 사용자가 100만 명이라면 100만 개의 시계열이 만들어지는 셈입니다.
| 카디널리티 수준 | 예시 속성 값 | 시계열 수 |
|---|---|---|
| 저카디널리티 (권장) | http.method = "GET" |
소수 (5~10개) |
| 중카디널리티 | http.route = "/api/v1/users/{id}" |
수십~수백 개 |
| 고카디널리티 (위험) | user.id = "uuid-xxxx" |
수백만 개 이상 |
http.status_code는 고정된 수십 개의 값만 가지므로 저카디널리티에 해당합니다. 중카디널리티의 핵심은 라우트 패턴 수준(/api/v1/users/{id})이며, 여기에 실제 동적 값(/api/v1/users/12345)이 포함되기 시작하면 곧 고카디널리티로 전환됩니다.
두 프로세서의 역할 분담
| 목적 | 도구 | 예시 |
|---|---|---|
| 불필요한 스팬 전체 제거 | filterprocessor |
헬스체크, 내부 프로브 스팬 드롭 |
| 고카디널리티 속성 정제 | transformprocessor |
user.id, session.id 삭제·마스킹 |
| 고카디널리티 데이터포인트 선별 제거 | filterprocessor |
특정 라우트 패턴 메트릭 드롭 |
| 스팬 이름 정규화 | transformprocessor |
동적 경로를 메서드+라우트 패턴으로 통합 |
실전 적용
예시 1: 헬스체크·내부 프로브 스팬 전량 제거
헬스체크 엔드포인트 스팬은 서비스 가용성 확인 외에는 실질적인 가치가 없으면서도 전체 트레이스 볼륨의 상당 부분을 차지하는 경우가 많습니다.
processors:
filter:
error_mode: ignore
traces:
span:
- attributes["http.route"] == "/health"
- attributes["http.route"] == "/readiness"
- attributes["http.route"] == "/metrics"
- attributes["http.route"] == "/ping"
batch: {}
service:
pipelines:
traces:
receivers: [otlp]
processors: [filter, batch]
exporters: [otlp]| 설정 항목 | 역할 |
|---|---|
error_mode: ignore |
속성이 없는 스팬에서 평가 오류 발생 시 파이프라인 유지 |
| 조건 목록 (OR 논리) | 나열된 조건 중 하나라도 참이면 해당 스팬 즉시 드롭 |
batch 위치 |
filter 뒤에 배치해 줄어든 데이터만 배치 처리 |
예시 2: 고카디널리티 속성 정제 후 필터링 (transform → filter 조합)
속성을 먼저 정제한 뒤 필터링하는 패턴입니다. transformprocessor가 고카디널리티 속성을 제거하고, 이후 filterprocessor가 남은 불필요한 스팬을 걸러냅니다.
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
# 고카디널리티 개인 식별 속성 삭제 (PII 보호 겸용)
- delete_key(attributes, "user.id")
- delete_key(attributes, "session.id")
- delete_key(attributes, "request.id")
# 모든 속성 값을 최대 256자로 잘라내기
- truncate_all(attributes, 256)
filter:
error_mode: ignore
traces:
span:
# 속성 자체가 존재하지 않는 비-HTTP 스팬 제거
# == nil보다 IsPresent()가 더 명시적입니다 (값이 nil인 경우와 속성 부재를 명확히 구분)
- not(IsPresent(attributes["http.request.method"]))
batch: {}
service:
pipelines:
traces:
receivers: [otlp]
processors: [transform, filter, batch] # 순서 중요: transform 먼저
exporters: [otlp]파이프라인 순서가 왜 중요한가?
transform → filter순서로 배치하면 transform이 속성을 정제한 상태에서 filter 조건이 평가됩니다. 반대로 배치하면 아직 정제되지 않은 원본 속성으로 필터 조건이 평가되어 의도치 않은 결과가 나올 수 있습니다.
| 설정 항목 | 역할 |
|---|---|
delete_key() |
속성 키-값 쌍 완전 삭제 |
truncate_all(attributes, 256) |
모든 속성 값을 256자로 제한해 대용량 페이로드 방지 |
not(IsPresent(...)) |
속성이 아예 존재하지 않는 경우를 명시적으로 확인 |
예시 3: 메트릭 카디널리티 폭발 제어
http.route 속성에 사용자 ID 패턴이 포함된 데이터포인트만 선별 제거하는 예시입니다. 메트릭 자체는 보존하면서 카디널리티를 유발하는 특정 패턴만 드롭합니다.
이 예시에서는 and 키워드로 두 조건을 결합했습니다. 특정 메트릭 이름과 라우트 패턴 모두 충족할 때만 드롭하도록 범위를 좁혀 의도치 않은 삭제를 방지합니다.
processors:
filter:
error_mode: ignore
metrics:
datapoint:
- metric.name == "http.server.request.duration" and
IsMatch(attributes["http.route"], ".*/users/[0-9]+.*")
- metric.name == "http.server.request.duration" and
IsMatch(attributes["http.route"], ".*/orders/[0-9]+.*")
batch: {}
service:
pipelines:
metrics:
receivers: [otlp]
processors: [filter, batch]
exporters: [prometheus]| 설정 항목 | 역할 |
|---|---|
metric.name == "..." |
특정 메트릭만 대상으로 조건 적용 |
IsMatch(..., regex) |
정규식으로 고카디널리티 라우트 패턴 매칭 |
and 조합 |
두 조건 모두 충족할 때만 드롭 (의도치 않은 삭제 방지) |
예시 4: span.name 정규화로 스팬 메트릭 카디널리티 제어
스팬 이름(span name)이 /api/v1/users/12345처럼 동적 값을 포함하면, 스팬에서 파생되는 메트릭의 카디널리티가 폭발합니다. HTTP 메서드와 정규화된 라우트를 합쳐 일관된 이름으로 통합하면 카디널리티를 대폭 낮출 수 있습니다.
processors:
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
# HTTP 메서드와 정규화된 라우트를 조합해 저카디널리티 스팬 이름 생성
# 결과 예: "GET /api/v1/users/{id}"
# http.route는 OTel Semantic Convention에 따라 이미 템플릿 형식으로 설정됩니다
- set(name, Concat([attributes["http.request.method"], " ", attributes["http.route"]], ""))
where IsMatch(name, "^/api/v[0-9]+/.*")
batch: {}
service:
pipelines:
traces:
receivers: [otlp]
processors: [transform, batch]
exporters: [otlp]OTel Semantic Convention에서 http.route는 실제 경로가 아닌 템플릿 형태(/api/v1/users/{id})로 정의됩니다. 따라서 메서드와 라우트를 합치면 GET /api/v1/users/{id}처럼 서로 다른 엔드포인트를 구분하면서도 저카디널리티를 유지하는 스팬 이름이 만들어집니다. 단순히 메서드만(GET)으로 치환하면 서로 다른 엔드포인트가 같은 스팬 이름을 공유해 오히려 관측 가능성을 해칠 수 있으니 주의하시기 바랍니다.
완전한 설정 파일 예시
OTel Collector를 처음 접하시는 분을 위해 receivers와 exporters를 포함한 최소 완전 설정 파일 예시를 살펴봅니다. Docker나 Helm으로 Collector를 실행하는 방법은 공식 Getting Started 가이드를 참고하시면 됩니다.
# config.yaml — filter + transform 조합 완전 예시
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
filter:
error_mode: ignore
traces:
span:
- attributes["http.route"] == "/health"
- attributes["http.route"] == "/readiness"
- attributes["http.route"] == "/ping"
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
- delete_key(attributes, "user.id")
- delete_key(attributes, "session.id")
- truncate_all(attributes, 256)
batch:
send_batch_size: 1000
timeout: 5s
exporters:
otlp:
endpoint: "http://jaeger:4317"
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [filter, transform, batch]
exporters: [otlp]장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 비용 절감 | 불필요한 스팬·메트릭 제거로 저장소·쿼리 비용을 직접 절감 |
| 쿼리 성능 향상 | 카디널리티가 낮을수록 집계 쿼리 속도가 빨라짐 |
| 백엔드 독립적 | Collector 레이어에서 처리하므로 특정 백엔드에 종속되지 않음 |
| 실시간 적용 | 앱 재배포 없이 Collector 설정 변경만으로 즉시 반영 가능 |
| 보안·컴플라이언스 | PII 속성을 백엔드 도달 전에 삭제해 데이터 유출 방지 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 고아 텔레메트리 위험 | 부모 스팬을 드롭하면 자식 스팬이 컨텍스트 없이 남아 트레이스 무결성 파괴 | 부모·자식 스팬을 함께 드롭하거나 tail-sampling processor와 조합 |
| OTTL 성능 비용 | 정규식(regex) 패턴은 CPU를 많이 소모 | 고트래픽 환경에서 부하 테스트 필수, 정적 조건 우선 사용 |
| 드롭의 불가역성 | 한번 드롭된 데이터는 복구 불가 | 처음에는 debug exporter로 드롭 대상을 확인 후 단계적으로 적용 |
| 샘플러 순서 민감성 | filterprocessor를 tail-sampling processor 뒤에 배치하면 트레이스 무결성 파괴 | 아래 내용 참고 |
tail-sampling processor와의 조합 패턴:
filterprocessor를 tail-sampling processor 앞에 배치해 노이즈성 스팬을 먼저 제거한 후 샘플링 결정을 내리는 패턴이 표준으로 자리잡고 있습니다. 권장 순서는filter → tail-sampling → batch입니다. tail-sampling은 트레이스가 완전히 수집된 후 전체 트레이스 단위로 샘플링 여부를 결정하기 때문에, filter가 먼저 노이즈를 걷어낼수록 더 의미 있는 샘플링 결정을 내릴 수 있습니다.
실무에서 가장 흔한 실수
-
error_mode: ignore를 빠뜨리는 것 — 속성이 없는 스팬에서 조건 평가 오류가 발생해 파이프라인 전체가 중단될 수 있습니다. 프로덕션에서는 모든 프로세서에 설정하는 것을 권장합니다. -
정규식 조건을 과도하게 사용하는 것 —
IsMatch()는 CPU 비용이 높습니다. 정적 문자열 비교(==)로 처리할 수 있는 경우라면 정규식 대신 정적 조건을 사용하시는 편이 좋습니다. -
조건을 충분히 검증하지 않고 처음부터 드롭을 적용하는 것 — 너무 넓은 조건은 예상치 못한 데이터 손실을 유발하며, 삭제된 데이터는 복구가 불가능합니다.
debug exporter로 드롭 대상 데이터를 먼저 확인하고,and키워드로 조건을 정밀하게 좁혀가는 방식을 권장합니다.
마치며
filterprocessor로 가치 없는 스팬을 파이프라인 초입에서 제거하고 transformprocessor로 고카디널리티 속성을 정제하는 두 단계 조합을 적용하고 나면, 백엔드에 도달하는 데이터는 더 작고, 더 쿼리하기 좋고, 더 의미 있는 상태로 바뀝니다.
지금 바로 시작해볼 수 있는 3단계:
-
현재 파이프라인에서 가장 많은 비중을 차지하는 스팬을 파악합니다. OTel Collector의
debugexporter를 활성화하거나, 백엔드에서http.route·span.name기준으로 스팬 볼륨 상위 엔드포인트를 집계해보시면 됩니다. -
헬스체크·내부 프로브 스팬부터
filterprocessor로 제거하는 설정을 작성합니다. 위 예시 1의 YAML을 실제 엔드포인트 경로에 맞게 수정한 뒤,processors배열 첫 번째에 배치하시면 됩니다. -
고카디널리티 속성을 식별하고
transformprocessor로 삭제 또는 정규화합니다.user.id,session.id,request.id처럼 고유값을 담는 속성을delete_key()로 제거하거나, OTTL Playground를 검색해 변환 조건을 사전 검증한 후 배포하는 것을 권장합니다.
다음 글: OTel Collector의
tail-sampling processor와filterprocessor를 조합해 에러·지연 트레이스는 100% 보존하면서 정상 트레이스 샘플링 비율을 동적으로 제어하는 2단계 파이프라인 설계
참고 자료
필수
- filterprocessor 공식 README | opentelemetry-collector-contrib
- transformprocessor 공식 README | opentelemetry-collector-contrib
- Transforming telemetry | OpenTelemetry 공식 문서
심화