OpenTelemetry 완벽 가이드 — 벤더 락인 없는 관측성 표준, 2026년 실전 구축
이 글의 핵심
OpenTelemetry(OTel)는 CNCF 졸업 직전 단계의 관측성 표준으로, "한 번 계측하면 어떤 백엔드에도 전송"을 가능케 합니다. 앱에서 OTLP로 내보내기만 하면 Datadog·Honeycomb·Grafana·Jaeger·뉴렐릭 어디든 전환 가능해 벤더 락인이 사라집니다. 이 글은 SDK·Collector·Context Propagation·주요 언어 예제·Kubernetes 운영·비용 최적화를 실전 중심으로 정리합니다.
OpenTelemetry의 세 기둥
| 시그널 | 설명 | 상태 |
|---|---|---|
| Traces | 분산 호출 추적 | GA |
| Metrics | 시계열 지표 | GA |
| Logs | 구조화 로그 | GA (2024+) |
모두 OTLP(gRPC/HTTP) 라는 단일 프로토콜로 전송됩니다.
아키텍처
앱 (OTel SDK + Instrumentation)
│ OTLP
▼
┌─────────────────────────┐
│ OTel Collector │
│ ┌─────┐ ┌──────┐ ┌────┐ │
│ │Recv │→│Proc │→│Exp │ │
│ └─────┘ └──────┘ └────┘ │
└─────────────────────────┘
│ │ │
▼ ▼ ▼
Datadog Honeycomb Grafana
(OTLP) (OTLP) Tempo/Loki/Mimir
- Receivers: OTLP·Jaeger·Zipkin·Prometheus scrape·syslog 등
- Processors: batch, attribute, filter, tail_sampling
- Exporters: OTLP, Prometheus, Jaeger, 벤더별 exporter
Node.js SDK + 자동 계측
// instrumentation.ts (앱 엔트리보다 먼저 로드)
import { NodeSDK } from "@opentelemetry/sdk-node"
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"
import { Resource } from "@opentelemetry/resources"
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "web-api",
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION,
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
}),
traceExporter: new OTLPTraceExporter({ url: "http://collector:4318/v1/traces" }),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: "http://collector:4318/v1/metrics" }),
exportIntervalMillis: 15_000,
}),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
process.on("SIGTERM", () => sdk.shutdown().finally(() => process.exit(0)))
node -r ./instrumentation.js dist/server.js
# 또는 TS: node --import ./instrumentation.mjs dist/server.js
HTTP·Express·Fastify·gRPC·pg·mysql·Redis·Kafka 등이 자동 계측됩니다.
수동 트레이싱
import { trace, SpanStatusCode } from "@opentelemetry/api"
const tracer = trace.getTracer("my-service")
async function checkout(userId: string) {
return await tracer.startActiveSpan("checkout", async (span) => {
try {
span.setAttribute("user.id", userId)
const cart = await loadCart(userId) // 자동 계측 child span
span.setAttribute("cart.items", cart.length)
const order = await createOrder(cart)
span.setAttribute("order.id", order.id)
return order
} catch (err) {
span.recordException(err as Error)
span.setStatus({ code: SpanStatusCode.ERROR })
throw err
} finally {
span.end()
}
})
}
커스텀 메트릭
import { metrics } from "@opentelemetry/api"
const meter = metrics.getMeter("my-service")
const orderCounter = meter.createCounter("orders_total", {
description: "Total orders placed",
})
const orderValue = meter.createHistogram("order_value_usd", {
description: "Order value in USD",
unit: "usd",
})
orderCounter.add(1, { plan: "pro" })
orderValue.record(99.9, { plan: "pro", country: "KR" })
Python 예시
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap --action=install
opentelemetry-instrument \
--traces_exporter otlp \
--metrics_exporter otlp \
--logs_exporter otlp \
--exporter_otlp_endpoint http://collector:4317 \
--service_name web-api \
python manage.py runserver
Django·Flask·FastAPI·SQLAlchemy·Redis 자동 계측. 별도 코드 변경 거의 없음.
Go 예시
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func handler(ctx context.Context, userID string) error {
tracer := otel.Tracer("my-service")
ctx, span := tracer.Start(ctx, "handler",
trace.WithAttributes(attribute.String("user.id", userID)))
defer span.End()
// ...
return nil
}
HTTP 미들웨어 otelhttp.NewHandler로 자동 계측.
Collector 설정
# otel-collector.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: 512
batch:
send_batch_size: 1024
timeout: 5s
attributes:
actions:
- key: http.request.header.authorization
action: delete
- key: environment
value: production
action: upsert
tail_sampling:
decision_wait: 10s
num_traces: 50000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1000 }
- name: sample_10pct
type: probabilistic
probabilistic: { sampling_percentage: 10 }
exporters:
otlphttp/honeycomb:
endpoint: https://api.honeycomb.io
headers:
x-honeycomb-team: ${HONEYCOMB_API_KEY}
otlp/datadog:
endpoint: otlp.datadoghq.com:4317
headers:
dd-api-key: ${DD_API_KEY}
prometheus:
endpoint: 0.0.0.0:9464
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
telemetry:
logs: { level: info }
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, attributes, batch]
exporters: [otlphttp/honeycomb]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
실행
otelcol-contrib --config otel-collector.yaml
Docker 이미지: otel/opentelemetry-collector-contrib:0.116.0 권장.
Kubernetes: OpenTelemetry Operator
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata: { name: otel-gateway, namespace: observability }
spec:
mode: deployment
replicas: 3
config:
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
# ... (동일)
---
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata: { name: java-instr }
spec:
exporter:
endpoint: http://otel-gateway-collector:4317
propagators: [tracecontext, baggage, b3]
sampler:
type: parentbased_traceidratio
argument: "0.25"
java:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
Pod에 annotation instrumentation.opentelemetry.io/inject-java: "true"만 추가하면 Java 앱이 자동 계측됩니다. Node·Python·.NET·Go 모두 유사.
Context Propagation
마이크로서비스 간 trace 연결의 핵심. 표준 traceparent 헤더(W3C Trace Context)가 기본.
HTTP Request
traceparent: 00-<trace_id>-<span_id>-<flags>
tracestate: vendor=xyz
모든 OTel SDK가 기본 생성·전파. Fetch·gRPC·message 큐 모두 자동.
Semantic Conventions
속성 이름을 표준화해 벤더 간 호환성 보장.
service.name,service.version,deployment.environmenthttp.request.method,http.response.status_code,http.routedb.system,db.statement,db.operationmessaging.system,messaging.destination
커스텀 키보다 표준 키 우선 사용. 2024-2025년 안정화가 크게 진전되어 모든 백엔드가 같은 키를 인식합니다.
비용 최적화 전략
Tail Sampling
전체 trace가 끝난 뒤 판단해 “에러·느린·샘플만” 저장. 전형적 감축율 90-99%.
Attribute Pruning
processors:
attributes:
actions:
- key: http.request.header.cookie
action: delete
- key: http.request.header.authorization
action: delete
Cardinality Control
메트릭 라벨 값 수를 제한:
processors:
metricstransform:
transforms:
- include: http_request_duration_seconds
action: update
operations:
- action: aggregate_labels
label_set: [service.name, http.route, http.response.status_code]
aggregation_type: sum
Logs 레벨 필터
processors:
filter/logs:
logs:
log_record:
- 'severity_number < SEVERITY_NUMBER_INFO'
토폴로지 선택
- Sidecar: 각 Pod에 Collector 사이드카. 격리·저지연 가능
- DaemonSet (Agent): 노드당 Collector. 모든 Pod이 localhost:4317로 전송
- Gateway (Deployment): 별도 Collector 풀. 샘플링·배치 집중
일반적으로 Agent + Gateway 2계층이 권장. Agent는 빠른 첫 홉, Gateway는 복잡한 처리·벤더 연결.
로그와 트레이스 연결
import { trace, context } from "@opentelemetry/api"
import pino from "pino"
const logger = pino({
mixin() {
const span = trace.getSpan(context.active())
const sc = span?.spanContext()
return sc ? { trace_id: sc.traceId, span_id: sc.spanId } : {}
},
})
logger.info("processing order")
// { trace_id: "...", span_id: "...", msg: "processing order" }
Grafana·Honeycomb 등에서 trace_id로 로그를 역추적 가능.
주요 백엔드 통합
| 백엔드 | 수신 |
|---|---|
| Datadog | OTLP (Agent 또는 직접) |
| Honeycomb | OTLP native |
| New Relic | OTLP native |
| Dynatrace | OTLP native |
| Grafana Tempo/Mimir/Loki | OTLP/Prometheus/Loki |
| Jaeger | OTLP native |
| Azure Monitor | OTLP (2024+) |
| AWS ADOT | OTLP, CloudWatch exporter |
| Elastic APM | OTLP native |
모두 OTLP를 말할 줄 아는 시대. 애플리케이션은 OTel만 알면 됩니다.
트러블슈팅
Span이 나오지 않음
- SDK 초기화를 앱 엔트리보다 먼저 로드 (
--require·--import) - exporter endpoint·프로토콜(gRPC vs HTTP) 확인
- Collector receiver가 활성
Cold start에 spans 손실
BatchSpanProcessor의 scheduled export 대기 중 프로세스 종료 →shutdown()호출 확인- 서버리스에서는
forceFlush()in finally
너무 많은 카디널리티
service.instance.id를 pod name으로 하면 폭발 → 의도적 설계
Collector OOM
memory_limiterprocessor 필수batch크기 조정- Gateway 샤딩
비용 폭증
- Tail Sampling 도입
- attribute cleanup
- 장기 보관 티어(S3)로 이관
체크리스트
-
service.name·service.version·deployment.environment를 모든 서비스에 설정 - 자동 계측 활성 + 핵심 로직만 수동 계측
- Collector를 Agent + Gateway로 구성
- Tail Sampling·attribute 정제로 비용 관리
- 로그에 trace_id·span_id 포함
- W3C Trace Context 전파 확인 (멀티 서비스)
- Operator·Instrumentation CRD로 K8s 자동 주입
- 비용·카디널리티 주기 모니터링
마무리
OpenTelemetry는 “관측성의 표준”이라는 10년 묵은 문제를 현실적으로 해결한 프로젝트입니다. 2026년 현재 트레이스·메트릭·로그 모두 GA 상태이고, 주요 상용·오픈소스 백엔드가 OTLP를 1급 수신 프로토콜로 지원해 벤더 락인 없이 관측 스택을 설계할 수 있는 시대가 열렸습니다. 자동 계측만 켜도 80점의 가치가 나오고, Collector로 비용·샘플링을 앱 외부에서 제어하면 조직 관측 예산의 상당 부분을 절감할 수 있습니다. 지금 Datadog/Splunk에 락인되어 있거나 자체 계측 코드가 분산돼 있다면 OTel로의 이주는 전략적 가치가 매우 큽니다.
관련 글
- Grafana Loki 로그 완벽 가이드
- Prometheus & Grafana 완벽 가이드
- Jaeger 트레이싱 가이드
- Datadog 완벽 가이드