Skip to content

Split View: OpenTelemetry 2026 심층 — OTLP·시맨틱 컨벤션·Collector 파이프라인·자동 계측의 표준화 전쟁이 끝난 자리

|

OpenTelemetry 2026 심층 — OTLP·시맨틱 컨벤션·Collector 파이프라인·자동 계측의 표준화 전쟁이 끝난 자리

프롤로그 — 표준화 전쟁이 끝났다

2018년 옵저버빌리티 풍경을 떠올려 보자. OpenTracingOpenCensus가 평행 우주에서 싸우고 있었고, 트레이스 데이터를 보내려면 벤더마다 다른 SDK(Datadog, New Relic, Lightstep, Honeycomb…)를 깔아야 했다. 라이브러리 작성자는 절망했다. 자기 라이브러리에 계측을 박아 넣고 싶어도, 어느 SDK를 골라야 한다.

2019년 OpenTracing과 OpenCensus가 OpenTelemetry로 합쳐졌다. 그게 7년 전 일이다.

2026년 5월의 풍경은 다르다.

  • OTLP는 사실상 유일한 옵저버빌리티 와이어 프로토콜이다. Tempo, Jaeger, Honeycomb, Datadog, New Relic, Dynatrace, Grafana Cloud, SigNoz — 전부 OTLP를 받는다. 벤더의 사설 프로토콜은 레거시 호환용으로만 남았다.
  • 시맨틱 컨벤션 v1이 잠겼다. HTTP, 관계형 DB, 메시징, RPC, 시스템 메트릭의 속성 이름들이 더 이상 바뀌지 않는다 — http.request.method, db.system.name, messaging.system. 대시보드를 짜는 사람이 한숨 돌렸다.
  • 로그 시그널이 GA가 됐다. 트레이스만 OTel이고 로그는 따로 보내는 시대가 끝났다.
  • Profiles가 네 번째 시그널로 진입했다. CNCF 인큐베이션을 거쳐 2024년 말부터 OTLP/profiles로 들어오는 데이터가 늘기 시작했다.
  • eBPF 자동 계측(Beyla, Coroot, OpenTelemetry eBPF Collector)이 'SDK 없이 트레이스가 나오는' 길을 열었다. 레거시 바이너리, 손 못 대는 서비스에 한 줄 트레이스가 가능해졌다.

요컨대 표준화 전쟁은 끝났다. 이제는 OTel을 어떻게 깔지의 문제다. 이 글은 OTLP 와이어 포맷부터 production Collector 파이프라인, 언어별 자동 계측, eBPF 경로의 트레이드오프까지 — 2026년 5월 기준 실제로 굴러가는 모양을 정확히 짚는다.


1장 · 풍경 — 왜 OpenTelemetry가 이겼는가

승부를 가른 결정은 셋이다.

  1. 벤더 중립 와이어 포맷. OTLP는 protobuf 스키마가 공개되어 있고, gRPC와 HTTP 둘 다 위에서 돈다. 어떤 벤더든 OTLP 엔드포인트만 열면 그 즉시 OTel 호환이 된다. 벤더 잠금이 약해진다 — 또는 정확히는, 잠금이 SDK가 아니라 백엔드(쿼리 언어, UI, 가격 정책)에 옮겨갔다.
  2. 시맨틱 컨벤션의 잠금. HTTP 요청을 어떻게 표현하는지(http.request.method, url.full, http.response.status_code), DB 쿼리를 어떻게 표현하는지(db.system.name, db.query.text)가 합의됐다. 이게 없었다면 OTel은 그냥 또 하나의 SDK였을 거다.
  3. CNCF 졸업. 2024년 OpenTelemetry는 Kubernetes에 이어 CNCF에서 가장 활발한 프로젝트 두 번째로 졸업한다. 의미가 컸다 — 회사 한 곳이 들고 가지 않는 진짜 중립 표준이라는 신호.

옛 풍경 vs 새 풍경

항목2019년 이전2026년
트레이스 표준OpenTracing + OpenCensus 평행OpenTelemetry 단일
와이어 프로토콜벤더마다 다름OTLP/gRPC + OTLP/HTTP
라이브러리 계측벤더 SDK에 박음OTel API에 박고 SDK는 갈아 끼움
로그별도 파이프라인(Fluentd 등)OTel 시그널 하나
메트릭Prometheus 별도 진영OTLP + Prometheus 호환
자동 계측일부 언어만자바·파이썬·노드·고·루비·.NET·PHP·러스트
시맨틱벤더별http.*, db.*, messaging.* 잠금

2장 · OTLP — 와이어 프로토콜이 가장 중요한 이유

OpenTelemetry의 핵심은 SDK가 아니라 OTLP다. OTLP는 트레이스·메트릭·로그·프로파일 데이터를 백엔드로 보내는 protobuf 스키마와 전송 프로토콜이다.

2.1 두 가지 트랜스포트

  • OTLP/gRPC — protobuf over HTTP/2. 기본 포트 4317. 가장 효율적. 서버 환경의 기본값.
  • OTLP/HTTP — protobuf 또는 JSON over HTTP/1.1. 기본 포트 4318. 브라우저, Lambda, 방화벽이 gRPC를 막는 환경에서 쓴다.

내용물(스키마)은 같다. 트랜스포트만 다르다. 이 분리가 중요하다 — 브라우저에서 직접 OTLP/HTTP로 트레이스를 보내는 게 가능해졌다.

2.2 와이어 포맷의 모양 (단순화)

message ExportTraceServiceRequest {
  repeated ResourceSpans resource_spans = 1;
}

message ResourceSpans {
  Resource resource = 1;          // 서비스 정보 (service.name, deployment.environment...)
  repeated ScopeSpans scope_spans = 2;
}

message ScopeSpans {
  InstrumentationScope scope = 1; // 어떤 라이브러리가 만든 스팬인지
  repeated Span spans = 2;
}

message Span {
  bytes trace_id = 1;
  bytes span_id = 2;
  string name = 5;
  fixed64 start_time_unix_nano = 7;
  fixed64 end_time_unix_nano = 8;
  repeated KeyValue attributes = 9;  // http.request.method = "GET" 등
  repeated Event events = 11;
  repeated Link links = 13;
  Status status = 15;
}

핵심은 ResourceScopeSpan의 세 단계 트리다. 같은 서비스의 같은 라이브러리에서 나온 스팬은 한 묶음으로 보내져 압축률이 좋다.

2.3 메트릭과 로그의 와이어 포맷

같은 패턴이 메트릭·로그·프로파일에 반복된다.

  • 메트릭: ResourceMetricsScopeMetricsMetric (Gauge / Sum / Histogram / ExponentialHistogram / Summary)
  • 로그: ResourceLogsScopeLogsLogRecord
  • 프로파일: ResourceProfilesScopeProfilesProfile (pprof 호환 스키마)

2.4 OTLP는 푸시인가 풀인가

OTLP는 푸시다. SDK 또는 Collector가 백엔드로 보낸다. 이건 Prometheus(풀)와 가장 큰 철학적 차이.

OTel은 Prometheus 호환을 위해 두 다리를 다 걸친다. Collector에 prometheusreceiver(타깃을 스크래이프)와 prometheusexporter(Prometheus가 와서 가져가게)가 모두 있다. 현실에선 메트릭만 Prometheus로, 트레이스·로그는 OTLP로 푸시하는 하이브리드가 흔하다.


3장 · 시맨틱 컨벤션 — 왜 이게 OTel의 진짜 무기인가

기술적으로 OTLP가 가장 멋있지만, 운영자가 매일 만지는 건 시맨틱 컨벤션이다.

3.1 무엇이 잠겼는가

2024년 9월부터 시작된 v1 stable 잠금 작업이 2025~2026년 사이 다음을 stable로 만들었다.

  • HTTPhttp.request.method, http.response.status_code, url.path, url.full, url.scheme, server.address, server.port, user_agent.original.
  • 데이터베이스db.system.name, db.namespace, db.query.text, db.collection.name, db.operation.name.
  • 메시징messaging.system, messaging.destination.name, messaging.operation.type, messaging.message.id.
  • RPCrpc.system, rpc.service, rpc.method.
  • 시스템 메트릭system.cpu.utilization, system.memory.usage, process.runtime.*.
  • 리소스service.name, service.version, service.instance.id, deployment.environment.name, host.name, os.type, cloud.provider, k8s.pod.name, k8s.namespace.name.

이게 stable로 잠겼다는 말은 — 이름이 더 이상 바뀌지 않는다는 뜻이다. 대시보드, 알람, 쿼리, 백엔드 통합 — 전부 안정해진다.

3.2 왜 잠금이 중요한가

OTel 초기에 잘 알려진 함정이 있었다. HTTP 속성 이름이 http.method였다가 http.request.method로 바뀌었다. 깔린 SDK는 모두 갈아 끼워야 했고, 대시보드는 다 깨졌다. 한 번이 아니라 여러 차례.

v1 잠금은 이 고통을 끝낸다. 이후의 변경은 v2 네임스페이스로 들어가고, v1은 보존된다. Kubernetes의 API 안정성 정책과 같은 원리.

3.3 시맨틱 컨벤션이 강제하는 것

  • 벤더 간 이동성. 같은 트레이스를 Tempo에서 Honeycomb으로 옮겨도 쿼리가 그대로 동작.
  • 공용 대시보드. Grafana 대시보드 마켓플레이스의 OTel 대시보드는 시맨틱 컨벤션 v1을 가정한다.
  • 라이브러리 계측의 신뢰성. 자동 계측이 만드는 속성 이름이 표준이라 운영자가 예측 가능하다.

4장 · 트레이스·메트릭·로그·프로파일 — 네 시그널의 현재

4.1 트레이스 (Tracing)

가장 성숙한 시그널. OTel 1.0의 핵심이고, 2026년에는 새로 들어올 것이 거의 없다. 트레이스의 단위는 스팬(span) — 시작·종료 시각, 속성, 이벤트, 부모 스팬을 가진 작업의 단위.

트레이스 ID로 묶여 분산 시스템 전체를 한 호출의 흐름으로 본다. traceparent 헤더(W3C Trace Context)로 서비스 경계를 넘어 컨텍스트가 전파된다.

4.2 메트릭 (Metrics)

OTel 메트릭은 Prometheus와는 모델이 약간 다르다. Prometheus는 'gauge·counter·histogram·summary'였고, OTel은 'Counter·UpDownCounter·Gauge·Histogram·ExponentialHistogram·ObservableX'로 세분화됐다.

큰 변화 하나 — ExponentialHistogram이 표준에 들어왔다. 기존 Histogram이 미리 정한 bucket boundaries를 쓰는 데 비해, ExponentialHistogram은 동적으로 분포에 맞춰진다. P99 같은 분위수 계산 정확도가 큰 폭으로 좋아진다.

Prometheus 진영도 native histograms로 같은 길을 갔다. 두 진영이 같은 결론에 도달.

4.3 로그 (Logs)

2024년 후반에 GA 진입. 의미는 — 별도 로그 파이프라인을 안 갖춰도 OTel만으로 트레이스·메트릭·로그가 다 보내진다는 것.

핵심 매력은 트레이스 컨텍스트 자동 부착이다. 로그 레코드에 trace_id·span_id가 자동으로 박혀, 트레이스 → 로그, 로그 → 트레이스 점프가 한 번 클릭으로 된다. 옛날엔 로그 라이브러리에 직접 코드를 박아야 했던 일이다.

다만 2026년에도 Fluent Bit / Vector가 죽지는 않았다. OTel의 로그 receiver는 강하지만, 컨테이너 로그 파일 테일링·파싱은 Fluent Bit이 더 성숙하다. 현실의 production은 'Fluent Bit이 파일을 읽어 OTLP로 Collector에 보냄' 또는 'Collector의 filelogreceiver가 직접 읽음'의 두 갈래다.

4.4 프로파일 (Profiles)

2024년 CNCF 인큐베이션을 거쳐 네 번째 시그널로 진입. 트레이스·메트릭·로그에 이어 컨티뉴어스 프로파일링이 OTel의 시그널로 들어왔다.

기술적 핵심: pprof 호환 스키마가 OTLP로 들어왔다. 이게 중요한 이유 — Grafana Pyroscope·Polar Signals Parca 등 기존 컨티뉴어스 프로파일링 도구들이 OTLP/profiles를 받게 되면 한 파이프라인으로 네 시그널을 다 보낼 수 있다.

2026년 5월 기준 상태:

  • 와이어 프로토콜(OTLP/profiles): stable에 가까운 베타.
  • SDK 지원: Go·Python·Java의 일부 자동 계측이 프로파일 시그널을 실험적으로 출력.
  • Collector 지원: otlpreceiver·otlpexporter가 profiles 시그널을 받는다. 일부 백엔드는 아직 미지원.
  • eBPF: Parca·Pyroscope가 eBPF로 pprof를 만들어 OTLP로 내보낸다.

요컨대 GA에 가깝지만 아직 완전하지 않다. 시작할 때 'profiles 활성화 가능한 SDK·Collector 버전'을 챙겨두면 좋다.


5장 · Collector 아키텍처 — 옵저버빌리티의 nginx

OpenTelemetry Collector는 OTel 생태계에서 가장 중요한 단일 컴포넌트다. 그 역할이 옵저버빌리티 파이프라인에서 nginx가 HTTP 트래픽에 하는 역할과 같다 — 수신·변환·라우팅.

5.1 세 가지 컴포넌트 타입

+-------------+      +-------------+      +-------------+
|  Receivers  | ---> | Processors  | ---> |  Exporters  |
+-------------+      +-------------+      +-------------+
   otlp                batch                otlp
   prometheus          memory_limiter        prometheusremotewrite
   filelog             attributes            elasticsearch
   jaeger              k8sattributes         loki
   zipkin              tail_sampling         tempo
   kafka               filter                kafka
                       transform
  • Receivers — 데이터를 받는 어댑터. OTLP가 기본이지만, Prometheus 스크레이프·Jaeger·Zipkin·Fluent Forward·Kafka·SQS 등 무엇으로든 받을 수 있다.
  • Processors — 받은 데이터를 변환·필터·보강한다. batch는 거의 필수, memory_limiter는 안전망, k8sattributes는 Kubernetes 메타데이터 자동 부착, tail_sampling은 샘플링 결정을 트레이스 완료 후로 미룬다.
  • Exporters — 백엔드로 내보낸다. OTLP가 기본이지만 백엔드별 전용 익스포터도 많다.

5.2 Core vs Contrib

Collector 배포가 두 갈래다.

  • otelcol (core) — 가장 핵심적인 컴포넌트만. 보안 감사 받기 좋고 바이너리가 작음.
  • otelcol-contrib (contrib) — 모든 커뮤니티 컴포넌트 포함. 99%의 production은 contrib을 쓴다.

권장: 처음엔 contrib으로 시작. 운영이 안정되면 OpenTelemetry Collector Builder (ocb)로 정확히 필요한 컴포넌트만 골라 커스텀 빌드.

5.3 파이프라인의 개념

Collector 설정은 pipelines로 묶인다. 시그널 타입별로 별도 파이프라인.

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlphttp/tempo]
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp, filelog]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlphttp/loki]

같은 receiver를 여러 파이프라인이 공유할 수 있고, fan-out·fan-in이 자유롭다.


6장 · Production Collector 설정 — sidecar → gateway → backend

실제 production 패턴 중 가장 흔한 모양은 두 단 계층이다.

+----------------+   OTLP   +-----------+   OTLP    +-----------+
| App + SDK      | -------> | Collector |---------->| Collector |
| (Pod sidecar)  |          | (sidecar) |           | (gateway) |
+----------------+          +-----------+           +-----------+
                                                          |
                                                          | OTLP / proprietary
                                                          v
                                                 +------------------+
                                                 | Tempo / Jaeger / |
                                                 | Honeycomb / etc. |
                                                 +------------------+
  • sidecar Collector (DaemonSet 또는 사이드카) — 로컬에서 빨리 받아 batch·재시도. 앱이 OTLP를 보내고 응답을 빨리 받아 자기 일을 한다.
  • gateway Collector (Deployment) — 클러스터 단위로 모아 tail sampling·메타데이터 보강·라우팅·다중 백엔드 fan-out.
  • backend — Tempo / Jaeger / SigNoz / Honeycomb / Datadog. OTLP를 받는 곳.

6.1 sidecar / agent Collector 설정 예

# otelcol-agent-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  prometheus:
    config:
      scrape_configs:
        - job_name: 'self-metrics'
          scrape_interval: 30s
          static_configs:
            - targets: ['localhost:8888']

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 400
    spike_limit_mib: 100
  k8sattributes:
    auth_type: serviceAccount
    passthrough: false
    extract:
      metadata:
        - k8s.pod.name
        - k8s.namespace.name
        - k8s.node.name
        - k8s.deployment.name
        - k8s.cluster.uid
  batch:
    timeout: 200ms
    send_batch_size: 8192

exporters:
  otlp/gateway:
    endpoint: otelcol-gateway.observability.svc.cluster.local:4317
    tls:
      insecure: true
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
    sending_queue:
      enabled: true
      num_consumers: 4
      queue_size: 5000

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlp/gateway]
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlp/gateway]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlp/gateway]

6.2 gateway Collector 설정 (tail sampling 포함)

gateway는 보통 트레이스 전체를 받고 결정한다 — 어떤 트레이스를 보존할지, 어디로 보낼지.

# otelcol-gateway-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 4000
    spike_limit_mib: 800
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-requests
        type: latency
        latency: { threshold_ms: 1000 }
      - name: sample-10pct
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }
  batch:
    timeout: 1s
    send_batch_size: 16384

exporters:
  otlphttp/tempo:
    endpoint: http://tempo-distributor.monitoring.svc.cluster.local:4318
  prometheusremotewrite:
    endpoint: http://mimir-distributor.monitoring.svc.cluster.local/api/v1/push
  otlphttp/loki:
    endpoint: http://loki-distributor.monitoring.svc.cluster.local:3100/otlp

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlphttp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/loki]

6.3 실전 운영 팁

  • memory_limiter는 첫 번째 프로세서로. 메모리가 폭주하면 다른 모든 프로세서가 무용지물.
  • tail sampling은 gateway에만. agent 레벨에서 하면 트레이스가 쪼개진다.
  • k8sattributes는 agent에 두는 게 정석. gateway엔 pod 정보가 안 닿을 수 있음.
  • batch 크기와 timeout은 백엔드 capacity에 맞춰 조정. 너무 작으면 RPS가 폭주, 너무 크면 latency가 늘어남.
  • Collector 자체 메트릭을 스크레이프하라. :8888/metricsotelcol_processor_dropped_spans 등이 있음.

7장 · 언어별 자동 계측 — 어디까지 자동으로 되나

OpenTelemetry의 가장 큰 매력 중 하나는 자동 계측(auto-instrumentation)이다. 코드 한 줄 안 건드리고 HTTP 핸들러·DB 드라이버·gRPC 클라이언트의 스팬이 나온다.

언어별 성숙도 차이가 크다.

7.1 Java — 가장 강력

opentelemetry-javaagent.jar이 사실상 산업 표준이다. JVM의 -javaagent 메커니즘으로 바이트코드를 런타임에 다시 쓰며, 100개 이상의 라이브러리(Spring, Hibernate, Apache HttpClient, Kafka, JDBC, Servlet, Reactor, gRPC, AWS SDK …)를 자동으로 계측한다.

java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.service.name=order-service \
     -Dotel.exporter.otlp.endpoint=http://otelcol-agent:4317 \
     -Dotel.exporter.otlp.protocol=grpc \
     -Dotel.resource.attributes=deployment.environment.name=prod \
     -jar app.jar

이게 끝이다. 코드 변경 0줄. Spring Boot 컨트롤러, 모든 JDBC 호출, Kafka producer·consumer의 스팬이 자동으로 나온다. 트레이스 컨텍스트 전파도 자동.

Java가 best-in-class인 이유 — JVM이 런타임 바이트코드 조작이 가장 자유롭고, OTel Java 팀이 가장 크고, Datadog Java agent의 노하우가 그대로 OTel로 합쳐졌다.

7.2 Python — opentelemetry-instrument

Python은 opentelemetry-instrument 런처와 opentelemetry-distro 패키지로 자동 계측한다. monkey-patching으로 라이브러리를 감싸는 방식.

pip install opentelemetry-distro opentelemetry-exporter-otlp \
            opentelemetry-instrumentation-flask \
            opentelemetry-instrumentation-requests \
            opentelemetry-instrumentation-psycopg2

opentelemetry-bootstrap -a install   # 깔린 라이브러리 자동 감지

OTEL_SERVICE_NAME=order-service \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol-agent:4317 \
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=prod \
opentelemetry-instrument python app.py

지원 라이브러리는 Flask, Django, FastAPI, Starlette, requests, urllib3, httpx, psycopg2, asyncpg, SQLAlchemy, Redis, pymongo, celery, kafka-python, boto3 등. Java만큼은 아니지만 일상적인 라이브러리는 거의 커버된다.

7.3 Node.js — require hook 기반

Node는 @opentelemetry/auto-instrumentations-node로 require hook을 걸어 자동 계측한다.

// tracing.js  ← 앱 진입점보다 먼저 로드돼야 한다
const { NodeSDK } = require('@opentelemetry/sdk-node')
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc')

const sdk = new NodeSDK({
  serviceName: 'order-service',
  traceExporter: new OTLPTraceExporter({
    url: 'http://otelcol-agent:4317',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
})

sdk.start()

실행:

node --require ./tracing.js app.js

ESM에서는 --import 플래그와 hook loader가 필요하다 (Node 20.6+의 register() API). CommonJS 환경이 자동 계측이 더 매끄럽다.

지원 라이브러리: Express, Koa, Fastify, Hapi, http, https, pg, mysql, redis, mongoose, ioredis, kafka.js, AWS SDK, GraphQL 등.

7.4 Go — 컴파일 타임 계측의 혁명

Go는 한참 동안 'OTel의 약점'이었다. Go는 monkey-patching이 어렵고, vtable이 없어 런타임 인터셉트가 깔끔하지 않다. 그래서 Go는 오랫동안 수동 계측만 가능했다.

2024~2025 사이에 두 길이 열렸다.

길 1: go build 시점에 코드 주입 — otel-go-instrumentation

go install github.com/open-telemetry/opentelemetry-go-instrumentation/cli@latest

# 빌드 시 자동 계측 주입
otel-instrument go build -o app ./...

빌드 도구가 표준 라이브러리와 인기 패키지(net/http, gRPC, database/sql 등)의 호출을 감싸는 래퍼를 자동 생성해 주입한다. 코드 변경 0줄.

길 2: eBPF로 외부에서 — Beyla

빌드 시 주입조차 안 한다. Beyla가 별도 프로세스로 떠서 eBPF로 커널이 보는 시스템 콜·소켓을 통해 트레이스를 만든다. 이쪽은 8장에서 자세히.

7.5 .NET, Ruby, PHP, Rust

  • .NETOpenTelemetry.AutoInstrumentation NuGet 패키지. CoreCLR profiler API로 IL을 다시 쓴다. Java 다음으로 성숙.
  • Rubyopentelemetry-instrumentation-all gem. monkey-patch 기반. Rails, Sinatra, Rack 등.
  • PHPopen-telemetry/opentelemetry-auto-laravel 등. OPcache 확장이 필요한 케이스 있음.
  • Rust — 자동 계측 거의 없음. tracing 크레이트의 OTel 어댑터로 수동 계측이 표준.

7.6 자동 계측의 한계

자동 계측은 HTTP·DB·메시지·RPC의 경계에서만 본다. 비즈니스 로직의 의미 있는 단위 — '주문 생성', '결제 검증', '재고 차감' — 는 보이지 않는다.

production OTel의 정답은 자동 계측 + 손으로 박는 비즈니스 스팬이다. 자동 계측이 인프라 경계를 다 잡아 주고, 손으로 박는 스팬은 비즈니스 의미를 더한다.


8장 · eBPF 자동 계측 — SDK 없이 트레이스가 나오는 길

2024~2025 사이 가장 큰 변화 중 하나. eBPF로 SDK를 안 깔고도 트레이스가 만들어진다.

8.1 작동 원리

eBPF 자동 계측 도구는 별도 프로세스로 떠서, 커널의 시스템 콜·소켓 이벤트를 훅한다. HTTP 요청, gRPC 호출, DB 연결을 직접 관찰한다.

  • 앱은 평범한 바이너리. SDK도 javaagent도 없다.
  • eBPF 프로그램이 accept·connect·read·write 등을 후크해 패킷을 디코드.
  • 디코드한 트랜잭션을 OTLP로 Collector에 보낸다.

8.2 도구들

  • Grafana Beyla — Grafana Labs가 만든 오픈소스. Go·Java·Node·Python·Rust 모두 같은 방식으로 본다. 트레이스·메트릭 둘 다 출력. 2024년 GA.
  • Coroot — Beyla보다 위에 있는 풀스택 옵저버빌리티 — eBPF로 수집하고 자기 UI를 가짐. OTel 호환.
  • OpenTelemetry eBPF Collector — OTel 공식 진영. 처음엔 Splunk가 만든 것이 OTel로 기증됨. 시스템 메트릭과 네트워크 단의 트레이스를 본다.
  • Pixie (Pixie Labs) — Kubernetes 전용. eBPF + 자기 쿼리 언어. OTel 출력 가능.
  • Cilium Hubble — 네트워크 계층에 집중. Cilium의 옵저버빌리티 컴포넌트.

8.3 eBPF 길의 강점

  • SDK 0줄. 레거시 바이너리, 손 못 대는 서드파티 서비스에 트레이스가 가능.
  • 계측 누락 위험이 0. 라이브러리 버전이 OTel과 안 맞아도 보인다.
  • 언어 무관. Beyla 하나로 Go·Java·Node·Python·Rust 트레이스가 같은 모양으로 나옴.
  • 운영 압력이 낮음. 앱을 다시 배포하지 않아도 적용.

8.4 eBPF 길의 한계

  • 분산 트레이스 컨텍스트 전파가 어려움. HTTP 헤더의 traceparent는 eBPF가 읽지만, 앱이 그것을 받아 내부 함수 호출까지 잇는 건 안 된다. 결과 — 서비스 단위 트레이스는 잘 나오지만 함수 단위 그래프는 부족.
  • 암호화된 트래픽. TLS 안의 HTTP/2를 보려면 uprobes로 라이브러리 함수를 후크해야 한다. 최근 도구들은 이걸 지원하지만 호환 매트릭스가 좁다.
  • 커널 권한 필요. Pod이 CAP_BPF 또는 privileged가 필요. 보안 정책에 따라 막힐 수 있음.
  • 비즈니스 로직 0. '주문 생성' 같은 도메인 의미는 잡지 못한다.

8.5 eBPF + SDK 하이브리드

production 정답은 eBPF + SDK 하이브리드다.

  • eBPF — 인프라 그림(서비스 간 호출, DB 쿼리, 외부 API 호출, 네트워크 latency).
  • SDK — 비즈니스 의미(도메인 스팬, 커스텀 메트릭, 비즈니스 로그).

두 데이터를 같은 trace_id로 묶으려면 — 어렵다. 현실은 'eBPF 트레이스 그래프 + SDK 트레이스 그래프'가 따로 노는 경우가 많다. 2026년 5월 기준 이 부분이 가장 활발히 개선되는 중.


9장 · 리소스 검출 (Resource Detection)

OTel 신호의 모든 데이터는 Resource에 묶여 송신된다. 'service.name=order-service, deployment.environment.name=prod, host.name=node-01, k8s.pod.name=order-69b' 같은 메타데이터.

이걸 손으로 다 박을 수도 있지만, OTel SDK와 Collector에는 자동 리소스 검출기가 있다.

9.1 검출기 종류

  • 환경 변수OTEL_RESOURCE_ATTRIBUTES=service.name=order,deployment.environment.name=prod.
  • 프로세스 — pid, command, runtime version.
  • 호스트 — hostname, OS, architecture.
  • 컨테이너 — container.id (cgroup에서 추출).
  • KubernetesdownwardAPI로 env vars 주입, 또는 Collector의 k8sattributes 프로세서가 pod IP로 보강.
  • 클라우드 — AWS EC2/ECS/EKS/Lambda, GCP GCE/GKE/Cloud Run, Azure VM/AKS의 IMDS를 호출해 인스턴스 메타데이터 검출.

9.2 우선순위

OTel 사양은 우선순위를 정해 둔다 — 환경 변수 < SDK 디폴트 < 명시적 코드 설정. 클라우드 검출기는 SDK 시작 시 자동으로 돈다.

9.3 production 패턴

env:
  - name: OTEL_SERVICE_NAME
    value: order-service
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: "deployment.environment.name=prod,service.version=$VERSION"
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  - name: OTEL_RESOURCE_ATTRIBUTES_OTHER
    value: "k8s.pod.name=$POD_NAME,k8s.node.name=$NODE_NAME"

또는 Collector의 k8sattributes 프로세서에 맡기는 게 더 깔끔. SDK 쪽은 service.name만 챙기고, 나머지는 Collector가 pod IP → API server 조회로 자동 보강.


10장 · 벤더 SDK에서 OTel SDK로 — 마이그레이션 이야기

2024~2025 사이 가장 많이 본 마이그레이션 패턴.

10.1 시나리오 — Datadog dd-trace에서 OTel로

이미 Datadog dd-trace로 트레이스를 보내는 코드베이스. OTel로 가고 싶다. 두 길.

길 1: dd-trace의 OTel API 호환 모드

dd-trace 자체가 OTel API를 받는 모드가 있다. 코드는 OTel API로 작성하지만, dd-trace가 와이어 포맷을 처리.

[코드: OTel API] -> [dd-trace agent: OTel API 받음] -> [Datadog 백엔드: dd-trace 포맷]

장점: 단계적 이동. 코드는 OTel API로 미리 갈아 두고, 백엔드는 천천히. 단점: dd-trace를 결국 떼지 않으면 의미가 적음.

길 2: OTel SDK 직출

[코드: OTel API] -> [OTel SDK] -> [OTLP] -> [OTel Collector] -> [Datadog OTLP 엔드포인트 또는 백엔드 갈아끼움]

장점: 진짜 OTel. 백엔드 갈아끼우기가 자유. 단점: 한 번에 옮겨야. dd-trace의 일부 자동 계측이 OTel에 아직 없는 경우 갭이 생긴다.

10.2 단계적 마이그레이션 순서

  1. OTel Collector 먼저 세움. dd-trace agent와 병행 운영.
  2. 새 서비스부터 OTel SDK. dd-trace는 새 서비스에 추가하지 않음.
  3. 트레이스 → 메트릭 → 로그 순으로 이동. 트레이스가 가장 매끄럽고, 로그는 dashboard 호환이 가장 까다롭다.
  4. dashboard 재작성. 시맨틱 컨벤션이 다르다 (http.status_code vs http.response.status_code). 한 번에 다 바꾸지 말고 새 dashboard부터.
  5. dd-trace agent 제거. 모든 서비스가 OTel SDK로 옮겨진 뒤.

10.3 솔직한 트레이드오프

OTel SDK가 모든 면에서 벤더 agent보다 낫지는 않다.

항목OTel SDK벤더 SDK (예: dd-trace)
자동 계측 폭좋음 (Java best-in-class)매우 넓음 (오랜 누적)
런타임 오버헤드일반적으로 약간 더 큼오래 튜닝되어 더 가볍기도
백엔드 자유도매우 큼한 벤더에 묶임
시맨틱 일관성표준 시맨틱 컨벤션벤더 고유 이름
진단·서포트커뮤니티벤더 SRE 지원
AI/ML 워크플로신생 (OTel GenAI 컨벤션)일부 벤더가 앞서 있음

요컨대 OTel은 잠금을 푸는 대신 약간의 런타임 비용을 가져간다. 이게 받아들일 만한가 — 답은 보통 'Yes'다. 백엔드 자유도가 SDK 미세 튜닝의 가치를 크게 능가하는 경우가 많다.


11장 · GenAI 시맨틱 컨벤션 — 다음에 잠겨야 할 영역

2025~2026 사이 OTel 진영에서 가장 활발한 작업 영역. LLM 호출의 추적을 위한 시맨틱 컨벤션이 잠금 직전까지 왔다.

핵심 속성(베타, 곧 stable 예정).

  • gen_ai.systemopenai, anthropic, google, ollama, …
  • gen_ai.request.modelclaude-3.5-sonnet, gpt-4o, …
  • gen_ai.usage.input_tokens / gen_ai.usage.output_tokens — 비용·rate-limit 추적.
  • gen_ai.response.finish_reasonsstop, length, tool_calls, …
  • gen_ai.operation.namechat, tool_call, embedding, text_completion.
  • 이벤트 — 메시지 단위 입출력(선택, 비용·프라이버시 트레이드오프).

LangChain, LlamaIndex, OpenLLMetry, Arize Phoenix 등이 이 컨벤션을 따라간다. LLM 워크플로의 비용·latency·실패율을 표준화된 모양으로 보게 된다. 다음 한두 분기 안에 v1 stable로 잠길 가능성이 크다.


12장 · 흔한 실수와 안티 패턴

production에서 자주 보는 실수들.

12.1 SDK 직접 백엔드로 보내기

앱 SDK ----(OTLP)----> Datadog / Honeycomb

작은 환경에서는 동작한다. 큰 환경에서 문제 — 앱 재배포 없이 백엔드 정책을 못 바꾼다 (샘플링, 라우팅, 보강). 항상 Collector를 한 단 끼워야 한다.

12.2 head sampling으로 에러 트레이스 놓침

SDK가 1% 샘플링을 하면, 에러 트레이스의 99%도 같이 사라진다. 에러는 무조건 보존해야 하니 tail sampling(Collector에서)을 써야 한다.

12.3 trace ID·span ID를 직접 만듦

직접 16바이트 UUID를 만들고 trace_id로 박는 코드를 봤다 — 표준 트레이스 컨텍스트 전파와 안 맞는다. OTel API의 getCurrentSpan().spanContext()를 쓰라.

12.4 매 요청에 새 Tracer 생성

// 안 좋은 예
function handler(req) {
  const tracer = trace.getTracer('app')  // 매번 새로 받는 듯 보임
  tracer.startSpan(...)
}

getTracer는 캐시되지만, 분명히 모듈 최상단에서 한 번만 받는 게 의도 표현이 깔끔하다.

12.5 batch 없이 OTLP 직출

batch 프로세서 없이 보내면 RPS만큼의 작은 요청이 나간다. batch 프로세서는 사실상 필수.

12.6 PII가 속성에 들어감

http.request.bodyuser.email을 그대로 속성으로 박으면 옵저버빌리티 백엔드에 평문 PII가 쌓인다. attributes 프로세서로 마스킹하거나 아예 빼라.

12.7 ExponentialHistogram 안 쓰고 고정 bucket로 P99

기본 Histogram이 5ms / 10ms / 50ms / 100ms / 500ms 같은 고정 bucket을 쓰면 P99 계산이 부정확하다. ExponentialHistogram을 디폴트로.

12.8 Collector를 단일 인스턴스로 SPOF

agent는 DaemonSet, gateway는 HPA. 항상 다중 인스턴스. gateway가 죽으면 그 시각 모든 신호가 사라진다.


에필로그 — 표준이 깔린 자리에서 무엇을 할 것인가

OpenTelemetry는 2026년 5월 기준 표준화 전쟁에서 이긴 자리에 있다. 다만 'OTel을 쓰는가'는 더 이상 흥미로운 질문이 아니다. 흥미로운 질문은 다음이다.

  • 얼마나 잘 깔았는가. agent + gateway 두 단인가, 단일 인스턴스인가. tail sampling이 있는가. memory_limiter가 첫 번째 프로세서인가.
  • 시맨틱 컨벤션을 따르는가. 자동 계측만 믿지 않고 비즈니스 스팬도 시맨틱 규약으로 박는가.
  • 자동 계측 + 손 계측의 균형. 인프라 경계는 자동, 비즈니스 의미는 손으로.
  • eBPF를 어디에 쓰는가. 레거시·서드파티에만 쓰는가, 아니면 SDK 쪽이 전혀 안 되는 환경의 메인으로 쓰는가.
  • profiles 시그널을 받을 준비. 백엔드와 SDK 버전이 받는가.

도입 체크리스트

  • Collector를 agent + gateway 두 단으로 구성했는가.
  • memory_limiter가 모든 파이프라인의 첫 번째 프로세서인가.
  • tail sampling이 gateway에 있고 에러·느린 요청은 무조건 보존되는가.
  • k8sattributes 프로세서가 agent에서 도는가.
  • batch 프로세서가 모든 파이프라인에 있는가.
  • 자동 계측이 깔린 언어는 javaagent / opentelemetry-instrument / require hook / 컴파일 타임 주입 중 무엇으로 도는가.
  • 비즈니스 스팬에 시맨틱 컨벤션이 적용됐는가.
  • PII가 속성에 들어가지 않게 attributes 프로세서가 마스킹하는가.
  • Collector의 self-metrics를 스크레이프하는가 (otelcol_processor_dropped_spans 등).
  • ExponentialHistogram을 디폴트로 쓰는가.
  • 백엔드가 GenAI 시맨틱 컨벤션을 받을 준비가 되었는가.

안티 패턴 정리

  1. SDK가 직접 백엔드로 — Collector를 한 단 끼우라.
  2. head sampling만 — 에러 트레이스를 잃는다. tail sampling 추가.
  3. batch 프로세서 없이 OTLP — RPS만큼의 작은 요청.
  4. memory_limiter를 마지막에 — 메모리가 폭주하면 의미 없다. 첫 번째로.
  5. PII가 그대로 속성에 — attributes 프로세서로 마스킹.
  6. 자동 계측만 믿고 비즈니스 스팬 0줄 — 인프라 경계만 보임.
  7. Collector를 단일 인스턴스 — SPOF. 다중 인스턴스 필수.
  8. 시맨틱 컨벤션을 무시한 커스텀 속성 — 대시보드 호환이 깨진다.
  9. 트레이스만 보내고 메트릭·로그는 다른 파이프라인 — OTel 시그널 세 개를 한 파이프라인으로 묶을 때 가장 큰 가치가 난다.
  10. profiles 시그널을 받을 백엔드가 없는데 SDK에서 켬 — 데이터가 어디로도 안 간다.

다음 글 예고

다음 글 후보: Collector 운영 깊게 — 파이프라인 부하·드롭·메트릭 카디널리티의 함정, OTel Profiles 실전 — eBPF로 Go 바이너리의 hotspot 잡기, GenAI 시맨틱 컨벤션으로 LLM 비용·latency·실패율을 단일 대시보드로 보는 법.


참고 / References

OpenTelemetry 2026 Deep Dive — OTLP, Semantic Conventions, the Collector Pipeline, and Auto-Instrumentation After the Standardization War

Prologue — The Standardization War Is Over

Picture the observability landscape in 2018. OpenTracing and OpenCensus were fighting in parallel universes, and to send trace data you had to install a different SDK for every vendor (Datadog, New Relic, Lightstep, Honeycomb, etc.). Library authors despaired. They wanted to bake in instrumentation, but had to pick a vendor.

In 2019, OpenTracing and OpenCensus merged into OpenTelemetry. That was seven years ago.

May 2026 looks different.

  • OTLP is effectively the only observability wire protocol. Tempo, Jaeger, Honeycomb, Datadog, New Relic, Dynatrace, Grafana Cloud, SigNoz — all accept OTLP. Vendor-specific protocols hang around only for legacy compatibility.
  • Semantic conventions v1 are locked. The attribute names for HTTP, relational DB, messaging, RPC, and system metrics do not change anymore: http.request.method, db.system.name, messaging.system. Dashboard authors exhaled.
  • The log signal is GA. The era of "traces in OTel, logs somewhere else" is over.
  • Profiles has entered as the fourth signal. After CNCF incubation, OTLP/profiles data started growing from late 2024.
  • eBPF auto-instrumentation (Beyla, Coroot, OpenTelemetry eBPF Collector) opened a path where traces appear without any SDK. A line of trace data for legacy binaries and untouchable services became real.

In short, the standardization war is over. The question is now how you deploy OTel. This piece walks from the OTLP wire format through a production Collector pipeline, per-language auto-instrumentation, and the tradeoffs of the eBPF path — exactly the shape that is shipping in May 2026.


1. The Landscape — Why OpenTelemetry Won

Three decisions tipped the fight.

  1. A vendor-neutral wire format. OTLP's protobuf schema is public, and it runs over both gRPC and HTTP. Any vendor that opens an OTLP endpoint becomes OTel-compatible immediately. Vendor lock weakens — or to be precise, lock-in moves from the SDK to the backend (query language, UI, pricing).
  2. A semantic convention lock. How to represent an HTTP request (http.request.method, url.full, http.response.status_code) and a DB query (db.system.name, db.query.text) got agreed. Without this, OTel would have been just another SDK.
  3. CNCF graduation. In 2024 OpenTelemetry graduated as the second-most-active CNCF project after Kubernetes. The signal mattered — this is a real neutral standard, not something owned by one company.

Old landscape vs new

ItemPre-20192026
Tracing standardOpenTracing + OpenCensus in parallelOpenTelemetry single
Wire protocolOne per vendorOTLP/gRPC + OTLP/HTTP
Library instrumentationBaked into a vendor SDKBaked into the OTel API, SDK is swappable
LogsSeparate pipeline (Fluentd, etc.)One OTel signal
MetricsSeparate Prometheus campOTLP + Prometheus compat
Auto-instrumentationA few languagesJava, Python, Node, Go, Ruby, .NET, PHP, Rust
SemanticsPer-vendorhttp.*, db.*, messaging.* locked

2. OTLP — Why the Wire Protocol Matters Most

The heart of OpenTelemetry is not the SDK; it is OTLP. OTLP is the protobuf schema and transport protocol that ships trace, metric, log, and profile data to a backend.

2.1 Two transports

  • OTLP/gRPC — protobuf over HTTP/2. Default port 4317. Most efficient. The default in server environments.
  • OTLP/HTTP — protobuf or JSON over HTTP/1.1. Default port 4318. Used in browsers, Lambda, and environments where firewalls block gRPC.

The payload (schema) is identical; only the transport differs. This separation matters — sending traces directly from a browser over OTLP/HTTP became possible.

2.2 Wire shape (simplified)

message ExportTraceServiceRequest {
  repeated ResourceSpans resource_spans = 1;
}

message ResourceSpans {
  Resource resource = 1;          // service info (service.name, deployment.environment...)
  repeated ScopeSpans scope_spans = 2;
}

message ScopeSpans {
  InstrumentationScope scope = 1; // which library produced the spans
  repeated Span spans = 2;
}

message Span {
  bytes trace_id = 1;
  bytes span_id = 2;
  string name = 5;
  fixed64 start_time_unix_nano = 7;
  fixed64 end_time_unix_nano = 8;
  repeated KeyValue attributes = 9;  // e.g. http.request.method = "GET"
  repeated Event events = 11;
  repeated Link links = 13;
  Status status = 15;
}

The key is the three-level ResourceScopeSpan tree. Spans from the same service and same library are bundled together, which compresses well.

2.3 Metrics and logs

The same pattern repeats for metrics, logs, and profiles.

  • Metrics: ResourceMetricsScopeMetricsMetric (Gauge / Sum / Histogram / ExponentialHistogram / Summary)
  • Logs: ResourceLogsScopeLogsLogRecord
  • Profiles: ResourceProfilesScopeProfilesProfile (pprof-compatible schema)

2.4 Is OTLP push or pull?

OTLP is push. The SDK or Collector pushes to a backend. That is the biggest philosophical break from Prometheus (pull).

OTel keeps both legs in for Prometheus compatibility. The Collector ships a prometheusreceiver (to scrape targets) and a prometheusexporter (so Prometheus can scrape the Collector). In real production, a hybrid — Prometheus for metrics, OTLP push for traces and logs — is common.


3. Semantic Conventions — Why This Is OTel's Real Weapon

Technically OTLP is the most impressive piece, but the operator touches semantic conventions every day.

3.1 What got locked

Lock-down work for v1 stable started in September 2024 and locked the following between 2025 and 2026:

  • HTTPhttp.request.method, http.response.status_code, url.path, url.full, url.scheme, server.address, server.port, user_agent.original.
  • Databasesdb.system.name, db.namespace, db.query.text, db.collection.name, db.operation.name.
  • Messagingmessaging.system, messaging.destination.name, messaging.operation.type, messaging.message.id.
  • RPCrpc.system, rpc.service, rpc.method.
  • System metricssystem.cpu.utilization, system.memory.usage, process.runtime.*.
  • Resourceservice.name, service.version, service.instance.id, deployment.environment.name, host.name, os.type, cloud.provider, k8s.pod.name, k8s.namespace.name.

Locked stable means the names do not change anymore. Dashboards, alerts, queries, backend integrations — everything stabilizes.

3.2 Why the lock matters

There was a well-known trap in early OTel. The HTTP attribute name was once http.method, then it changed to http.request.method. Every deployed SDK had to be replaced; every dashboard broke. This happened more than once.

The v1 lock ends that pain. Future changes go into a v2 namespace; v1 is preserved. Same principle as Kubernetes API stability.

3.3 What semantic conventions enforce

  • Vendor portability. Move the same trace data from Tempo to Honeycomb and the queries still work.
  • Shared dashboards. OTel dashboards in the Grafana marketplace assume semantic conventions v1.
  • Trustworthy library instrumentation. The attribute names emitted by auto-instrumentation are standard, so operators can predict them.

4. Traces, Metrics, Logs, Profiles — Current State of the Four Signals

4.1 Traces

The most mature signal. The core of OTel 1.0; very little new arrives in 2026. The unit of a trace is the span — a unit of work with a start and end time, attributes, events, and a parent span.

Spans are grouped by trace ID, so you see one request flowing across a distributed system. The traceparent header (W3C Trace Context) propagates context across service boundaries.

4.2 Metrics

OTel's metric model differs slightly from Prometheus. Prometheus has gauge / counter / histogram / summary; OTel subdivides into Counter / UpDownCounter / Gauge / Histogram / ExponentialHistogram / ObservableX.

One big change — ExponentialHistogram entered the standard. The old Histogram uses pre-chosen bucket boundaries; ExponentialHistogram adapts to the distribution dynamically. Quantile estimation accuracy (P99 and friends) improves a lot.

The Prometheus camp went the same way with native histograms. Both camps converged.

4.3 Logs

GA in late 2024. The point is — OTel alone now ships traces, metrics, and logs without a separate log pipeline.

The killer feature is automatic attachment of trace context. Log records get trace_id and span_id automatically, so trace-to-log and log-to-trace jumps are one click. In the old days you wired this by hand into your logger.

That said, Fluent Bit and Vector did not die in 2026. OTel's log receiver is strong, but Fluent Bit is more mature at tailing and parsing container log files. Real production splits two ways: Fluent Bit reads files and pushes OTLP to a Collector, or the Collector's filelogreceiver reads files directly.

4.4 Profiles

After CNCF incubation in 2024, profiling became the fourth signal. Continuous profiling joined OTel alongside traces, metrics, and logs.

The technical core: a pprof-compatible schema lands in OTLP. The reason this matters — Grafana Pyroscope, Polar Signals Parca, and other existing continuous-profiling tools, once they accept OTLP/profiles, let you ship all four signals through one pipeline.

Status as of May 2026:

  • Wire protocol (OTLP/profiles): near-stable beta.
  • SDK support: parts of Go, Python, and Java auto-instrumentation experimentally emit profile signals.
  • Collector support: otlpreceiver and otlpexporter accept the profiles signal. Some backends do not yet receive it.
  • eBPF: Parca and Pyroscope generate pprof via eBPF and ship it over OTLP.

Close to GA but not complete. When you start, pin SDK and Collector versions that can enable profiles.


5. Collector Architecture — The nginx of Observability

The OpenTelemetry Collector is the single most important component in the OTel ecosystem. Its role in an observability pipeline is what nginx is to HTTP traffic — receive, transform, route.

5.1 Three component types

+-------------+      +-------------+      +-------------+
|  Receivers  | ---> | Processors  | ---> |  Exporters  |
+-------------+      +-------------+      +-------------+
   otlp                batch                otlp
   prometheus          memory_limiter        prometheusremotewrite
   filelog             attributes            elasticsearch
   jaeger              k8sattributes         loki
   zipkin              tail_sampling         tempo
   kafka               filter                kafka
                       transform
  • Receivers — adapters that take data in. OTLP is the default, but you can take Prometheus scrapes, Jaeger, Zipkin, Fluent Forward, Kafka, SQS — anything.
  • Processors — transform, filter, and enrich. batch is essentially required, memory_limiter is a safety net, k8sattributes automatically attaches Kubernetes metadata, tail_sampling delays the sampling decision until the trace is complete.
  • Exporters — push to backends. OTLP is the default, but plenty of backend-specific exporters exist.

5.2 Core vs Contrib

Two distributions.

  • otelcol (core) — only the most essential components. Easier to security-audit; smaller binary.
  • otelcol-contrib (contrib) — all community components. 99% of production runs contrib.

Recommendation: start with contrib. Once operations stabilize, use OpenTelemetry Collector Builder (ocb) to build a custom image with exactly the components you need.

5.3 The concept of a pipeline

Collector config is grouped by pipelines. One pipeline per signal type.

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlphttp/tempo]
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp, filelog]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlphttp/loki]

The same receiver can feed multiple pipelines, and fan-out / fan-in are free.


6. Production Collector Setup — sidecar → gateway → backend

The most common real production pattern is a two-tier layout.

+----------------+   OTLP   +-----------+   OTLP    +-----------+
| App + SDK      | -------> | Collector |---------->| Collector |
| (Pod sidecar)  |          | (sidecar) |           | (gateway) |
+----------------+          +-----------+           +-----------+
                                                          |
                                                          | OTLP / proprietary
                                                          v
                                                 +------------------+
                                                 | Tempo / Jaeger / |
                                                 | Honeycomb / etc. |
                                                 +------------------+
  • sidecar Collector (DaemonSet or pod sidecar) — receives locally, batches, retries. The app sends OTLP and gets a fast response so it can get on with its work.
  • gateway Collector (Deployment) — aggregates cluster-wide for tail sampling, metadata enrichment, routing, and fan-out to multiple backends.
  • backend — Tempo / Jaeger / SigNoz / Honeycomb / Datadog. Wherever OTLP terminates.

6.1 sidecar / agent Collector config

# otelcol-agent-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  prometheus:
    config:
      scrape_configs:
        - job_name: 'self-metrics'
          scrape_interval: 30s
          static_configs:
            - targets: ['localhost:8888']

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 400
    spike_limit_mib: 100
  k8sattributes:
    auth_type: serviceAccount
    passthrough: false
    extract:
      metadata:
        - k8s.pod.name
        - k8s.namespace.name
        - k8s.node.name
        - k8s.deployment.name
        - k8s.cluster.uid
  batch:
    timeout: 200ms
    send_batch_size: 8192

exporters:
  otlp/gateway:
    endpoint: otelcol-gateway.observability.svc.cluster.local:4317
    tls:
      insecure: true
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
    sending_queue:
      enabled: true
      num_consumers: 4
      queue_size: 5000

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlp/gateway]
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlp/gateway]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, batch]
      exporters: [otlp/gateway]

6.2 gateway Collector config (with tail sampling)

The gateway usually receives the full trace and decides — which traces to keep, where to send them.

# otelcol-gateway-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 4000
    spike_limit_mib: 800
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-requests
        type: latency
        latency: { threshold_ms: 1000 }
      - name: sample-10pct
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }
  batch:
    timeout: 1s
    send_batch_size: 16384

exporters:
  otlphttp/tempo:
    endpoint: http://tempo-distributor.monitoring.svc.cluster.local:4318
  prometheusremotewrite:
    endpoint: http://mimir-distributor.monitoring.svc.cluster.local/api/v1/push
  otlphttp/loki:
    endpoint: http://loki-distributor.monitoring.svc.cluster.local:3100/otlp

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlphttp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/loki]

6.3 Operational tips

  • memory_limiter goes first. If memory blows up every other processor is useless.
  • tail sampling only at the gateway. Do it at the agent and traces fracture.
  • k8sattributes belongs on the agent. The gateway may not see pod info.
  • Tune batch size and timeout to backend capacity. Too small and RPS explodes; too large and latency climbs.
  • Scrape the Collector's own metrics. :8888/metrics exposes otelcol_processor_dropped_spans and friends.

7. Per-Language Auto-Instrumentation — How Automatic Is It

One of OTel's biggest selling points is auto-instrumentation. Without touching a line of code, spans appear for HTTP handlers, DB drivers, and gRPC clients.

Maturity varies sharply by language.

7.1 Java — best-in-class

The opentelemetry-javaagent.jar is effectively the industry standard. Using JVM's -javaagent mechanism it rewrites bytecode at runtime and auto-instruments more than 100 libraries (Spring, Hibernate, Apache HttpClient, Kafka, JDBC, Servlet, Reactor, gRPC, AWS SDK, …).

java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.service.name=order-service \
     -Dotel.exporter.otlp.endpoint=http://otelcol-agent:4317 \
     -Dotel.exporter.otlp.protocol=grpc \
     -Dotel.resource.attributes=deployment.environment.name=prod \
     -jar app.jar

That is it. Zero code changes. Spring Boot controllers, every JDBC call, every Kafka producer and consumer span shows up automatically. Trace context propagation is automatic too.

Why Java is best-in-class — JVM allows the freest runtime bytecode manipulation, the OTel Java team is the largest, and the know-how of the Datadog Java agent merged straight into OTel.

7.2 Python — opentelemetry-instrument

Python auto-instruments via the opentelemetry-instrument launcher and the opentelemetry-distro package. It wraps libraries via monkey-patching.

pip install opentelemetry-distro opentelemetry-exporter-otlp \
            opentelemetry-instrumentation-flask \
            opentelemetry-instrumentation-requests \
            opentelemetry-instrumentation-psycopg2

opentelemetry-bootstrap -a install   # auto-detect installed libs

OTEL_SERVICE_NAME=order-service \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol-agent:4317 \
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=prod \
opentelemetry-instrument python app.py

Supported libraries include Flask, Django, FastAPI, Starlette, requests, urllib3, httpx, psycopg2, asyncpg, SQLAlchemy, Redis, pymongo, celery, kafka-python, boto3. Not as deep as Java, but it covers the everyday libraries.

7.3 Node.js — require-hook based

Node auto-instruments through @opentelemetry/auto-instrumentations-node, which installs a require hook.

// tracing.js — must load before the app entry point
const { NodeSDK } = require('@opentelemetry/sdk-node')
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc')

const sdk = new NodeSDK({
  serviceName: 'order-service',
  traceExporter: new OTLPTraceExporter({
    url: 'http://otelcol-agent:4317',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
})

sdk.start()

Run it:

node --require ./tracing.js app.js

In ESM you need the --import flag and a hook loader (the register() API in Node 20.6+). CommonJS environments still auto-instrument more smoothly.

Supported libraries: Express, Koa, Fastify, Hapi, http, https, pg, mysql, redis, mongoose, ioredis, kafka.js, AWS SDK, GraphQL, etc.

7.4 Go — the compile-time instrumentation revolution

Go was an OTel weakness for a long time. Go is hard to monkey-patch, has no vtable, and runtime interception is messy. For years Go meant manual instrumentation only.

Two paths opened in 2024–2025.

Path 1: code injection at go build time — otel-go-instrumentation

go install github.com/open-telemetry/opentelemetry-go-instrumentation/cli@latest

# Inject auto-instrumentation at build time
otel-instrument go build -o app ./...

The build tool generates wrappers around standard library and popular package calls (net/http, gRPC, database/sql, etc.) and injects them. Zero source changes.

Path 2: outside the process via eBPF — Beyla

Even build-time injection is skipped. Beyla runs as a separate process and uses eBPF to observe syscalls and sockets in the kernel, producing traces. More on this in Section 8.

7.5 .NET, Ruby, PHP, Rust

  • .NETOpenTelemetry.AutoInstrumentation NuGet package. Rewrites IL via the CoreCLR profiler API. Second-most mature after Java.
  • Rubyopentelemetry-instrumentation-all gem. Monkey-patch based. Rails, Sinatra, Rack, etc.
  • PHPopen-telemetry/opentelemetry-auto-laravel and friends. Sometimes needs an OPcache extension.
  • Rust — virtually no auto-instrumentation. The OTel adapter for the tracing crate is the standard manual approach.

7.6 Limits of auto-instrumentation

Auto-instrumentation only sees the boundaries — HTTP, DB, message, RPC. The meaningful business units — "create order", "validate payment", "decrement inventory" — are invisible.

The right answer in production OTel is auto-instrumentation plus hand-coded business spans. Auto-instrumentation catches the infrastructure edges; manual spans add domain meaning.


8. eBPF Auto-Instrumentation — Traces Without an SDK

One of the largest shifts in 2024–2025. eBPF makes traces appear without any SDK installed.

8.1 How it works

An eBPF auto-instrumentation tool runs as a separate process and hooks into the kernel's syscalls and socket events. It directly observes HTTP requests, gRPC calls, and DB connections.

  • The app is a plain binary. No SDK, no javaagent.
  • An eBPF program hooks accept, connect, read, write, etc., and decodes the packets.
  • Decoded transactions are sent over OTLP to a Collector.

8.2 The tools

  • Grafana Beyla — Grafana Labs' open-source project. Watches Go, Java, Node, Python, and Rust uniformly. Emits both traces and metrics. GA in 2024.
  • Coroot — full-stack observability on top of Beyla — collects via eBPF and ships its own UI. OTel-compatible.
  • OpenTelemetry eBPF Collector — the official OTel project. Originally built by Splunk and donated. Watches system metrics and network-level traces.
  • Pixie (Pixie Labs) — Kubernetes-only. eBPF plus its own query language. OTel output supported.
  • Cilium Hubble — focused on the network layer. Cilium's observability component.

8.3 Strengths of the eBPF path

  • Zero SDK lines. Tracing works for legacy binaries and untouchable third-party services.
  • Zero instrumentation-miss risk. Visible even if a library version is out of sync with OTel.
  • Language-agnostic. One Beyla yields uniform Go, Java, Node, Python, and Rust traces.
  • Low operational pressure. No redeploy required.

8.4 Limits of the eBPF path

  • Distributed trace context propagation is hard. eBPF can read traceparent in HTTP headers, but cannot follow that context into function-level calls inside the app. Result — service-level traces look fine, function-level graphs are thin.
  • Encrypted traffic. To see HTTP/2 inside TLS you need uprobes hooking library functions. Modern tools support this but the compatibility matrix is narrow.
  • Kernel privileges required. Pods need CAP_BPF or privileged. Security policy may block it.
  • Zero business logic. Domain semantics like "create order" are missed.

8.5 eBPF + SDK hybrid

The right production answer is eBPF plus SDK.

  • eBPF — the infrastructure picture (service-to-service calls, DB queries, external API calls, network latency).
  • SDK — business meaning (domain spans, custom metrics, business logs).

Tying the two datasets together with the same trace_id is hard. In practice "eBPF trace graph + SDK trace graph" often run side by side without merging. This is the most actively improved area as of May 2026.


9. Resource Detection

Every piece of OTel data is shipped attached to a Resource — metadata like service.name=order-service, deployment.environment.name=prod, host.name=node-01, k8s.pod.name=order-69b.

You can set all of it by hand, but the OTel SDK and Collector include automatic resource detectors.

9.1 Kinds of detectors

  • Environment variablesOTEL_RESOURCE_ATTRIBUTES=service.name=order,deployment.environment.name=prod.
  • Process — pid, command, runtime version.
  • Host — hostname, OS, architecture.
  • Container — container.id (extracted from cgroup).
  • Kubernetes — env injection via downwardAPI, or the Collector's k8sattributes processor enriching by pod IP.
  • Cloud — AWS EC2/ECS/EKS/Lambda, GCP GCE/GKE/Cloud Run, Azure VM/AKS — detect instance metadata via IMDS.

9.2 Precedence

The OTel spec defines an order — environment variables less than SDK defaults less than explicit code settings. Cloud detectors run automatically at SDK startup.

9.3 Production pattern

env:
  - name: OTEL_SERVICE_NAME
    value: order-service
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: "deployment.environment.name=prod,service.version=$VERSION"
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  - name: OTEL_RESOURCE_ATTRIBUTES_OTHER
    value: "k8s.pod.name=$POD_NAME,k8s.node.name=$NODE_NAME"

Cleaner: hand it off to the Collector's k8sattributes processor. The SDK only sets service.name and the rest is enriched by the Collector via pod IP → API server lookup.


10. From Vendor SDK to OTel SDK — The Migration Story

The most common migration pattern of 2024–2025.

10.1 Scenario — Datadog dd-trace to OTel

You already ship traces via Datadog dd-trace and want to move to OTel. Two paths.

Path 1: dd-trace's OTel API compatibility mode

dd-trace itself can accept the OTel API. You write code against the OTel API; dd-trace handles the wire format.

[Code: OTel API] -> [dd-trace agent: accepts OTel API] -> [Datadog backend: dd-trace format]

Upside: incremental migration. Switch code to the OTel API now, switch the backend later. Downside: until you peel dd-trace off, you have not gained much.

Path 2: OTel SDK direct emit

[Code: OTel API] -> [OTel SDK] -> [OTLP] -> [OTel Collector] -> [Datadog's OTLP endpoint, or swap the backend]

Upside: real OTel. Free to swap backends. Downside: bigger jump. A gap appears wherever dd-trace had auto-instrumentation that OTel still lacks.

10.2 Staged migration order

  1. Stand up the OTel Collector first. Run alongside the dd-trace agent.
  2. Adopt the OTel SDK in new services. No new dd-trace.
  3. Migrate traces, then metrics, then logs. Traces are smoothest; logs are the hardest for dashboard compatibility.
  4. Rewrite dashboards. Semantic conventions differ (http.status_code vs http.response.status_code). Do new dashboards first, do not flip all at once.
  5. Remove the dd-trace agent. Only after every service is on OTel.

10.3 Honest tradeoffs

OTel SDK is not better than vendor agents on every axis.

AspectOTel SDKVendor SDK (e.g. dd-trace)
Auto-instrumentation breadthGood (Java best-in-class)Very broad (long accumulation)
Runtime overheadGenerally a touch heavierOften more tuned, sometimes lighter
Backend flexibilityVery largeLocked to one vendor
Semantic consistencyStandard semantic conventionsVendor-specific names
Diagnostics, supportCommunityVendor SRE support
AI/ML workflowsNew (OTel GenAI conventions)Some vendors are ahead

In short, OTel trades a little runtime cost to unlock the backend. Acceptable? Usually yes — the value of backend freedom typically dwarfs SDK micro-tuning gains.


11. GenAI Semantic Conventions — The Next Area to Lock

The most active area in the OTel camp through 2025–2026. Semantic conventions for tracing LLM calls are right before lock.

Core attributes (beta, near stable):

  • gen_ai.systemopenai, anthropic, google, ollama, ...
  • gen_ai.request.modelclaude-3.5-sonnet, gpt-4o, ...
  • gen_ai.usage.input_tokens / gen_ai.usage.output_tokens — cost and rate-limit tracking.
  • gen_ai.response.finish_reasonsstop, length, tool_calls, ...
  • gen_ai.operation.namechat, tool_call, embedding, text_completion.
  • Events — per-message inputs and outputs (optional, cost-and-privacy tradeoff).

LangChain, LlamaIndex, OpenLLMetry, Arize Phoenix all track these conventions. You will see LLM-workflow cost, latency, and failure rate in a standardized shape. v1 stable lock in the next quarter or two is likely.


12. Common Mistakes and Anti-Patterns

Mistakes seen often in production.

12.1 SDK pushing straight to the backend

App SDK ----(OTLP)----> Datadog / Honeycomb

Works in small environments. In large ones it fails — you cannot change backend policy (sampling, routing, enrichment) without redeploying the app. Always insert a Collector layer.

12.2 Losing error traces to head sampling

If the SDK head-samples at 1%, 99% of error traces vanish. Errors must be preserved, so use tail sampling (in the Collector).

12.3 Generating trace_id and span_id by hand

We saw code that made a 16-byte UUID and stuffed it as a trace_id — it breaks the standard trace context propagation. Use getCurrentSpan().spanContext() from the OTel API.

12.4 Creating a new Tracer per request

// bad
function handler(req) {
  const tracer = trace.getTracer('app')  // looks like a fresh one each time
  tracer.startSpan(...)
}

getTracer is cached, but pulling it once at module top is clearer about intent.

12.5 OTLP without batch

Without a batch processor, you send a small request per RPS. The batch processor is essentially required.

12.6 PII leaks into attributes

Putting http.request.body or user.email directly into attributes lands plain-text PII in the observability backend. Mask via the attributes processor or drop it.

12.7 Computing P99 from a fixed-bucket Histogram

A default Histogram with 5ms / 10ms / 50ms / 100ms / 500ms boundaries gives a sloppy P99. Default to ExponentialHistogram.

12.8 A single Collector instance as SPOF

Agent as DaemonSet, gateway as HPA. Always multi-instance. If the gateway dies, every signal in that window is gone.


Epilogue — What to Do Once the Standard Is Laid Down

OpenTelemetry sits in May 2026 in the spot of having won the standardization war. But "do you use OTel" is no longer an interesting question. The interesting ones are these:

  • How well did you deploy it. Two-tier agent + gateway, or single? Is there tail sampling? Is memory_limiter the first processor?
  • Do you follow semantic conventions. Not just trusting auto-instrumentation, but tagging business spans by the conventions too.
  • The balance of auto vs manual. Infrastructure edges automatic, business meaning manual.
  • Where you put eBPF. Only for legacy and third-party, or as the main path where the SDK side fails?
  • Are you ready for the profiles signal. Do your backend and SDK versions accept it?

Adoption checklist

  • Is the Collector deployed as a two-tier agent + gateway?
  • Is memory_limiter the first processor in every pipeline?
  • Is tail sampling in the gateway and are errors and slow requests unconditionally preserved?
  • Does the k8sattributes processor run on the agent?
  • Does every pipeline include the batch processor?
  • For instrumented languages, is it via javaagent, opentelemetry-instrument, require hook, or compile-time injection?
  • Do your business spans follow the semantic conventions?
  • Does the attributes processor mask PII out of attributes?
  • Do you scrape the Collector's self-metrics (otelcol_processor_dropped_spans, etc.)?
  • Is ExponentialHistogram the default?
  • Is your backend ready for GenAI semantic conventions?

Anti-pattern summary

  1. SDK direct to backend — insert a Collector layer.
  2. Head sampling only — loses error traces; add tail sampling.
  3. No batch processor — one small request per RPS.
  4. memory_limiter last — pointless if memory blows; put it first.
  5. PII in raw attributes — mask via attributes processor.
  6. Auto-instrumentation only, zero business spans — only the infrastructure edge is visible.
  7. Single Collector instance — SPOF. Multi-instance required.
  8. Custom attributes ignoring semantic conventions — dashboard compatibility breaks.
  9. Traces in OTel, metrics and logs in a different pipeline — biggest value is unifying all three signals.
  10. Enabling profiles in the SDK when the backend cannot receive it — data goes nowhere.

What is next

Next-piece candidates: Operating a Collector at depth — pipeline load, drops, and the trap of metric cardinality, OTel Profiles in practice — catching hotspots in a Go binary with eBPF, One dashboard for LLM cost, latency, and failure via GenAI semantic conventions.


References