TechFeedTechFeed
Open Source

OpenTelemetry 실전 가이드 — Node.js·Python 트레이싱 설정, Jaeger 연동, 프로덕션 관측가능성 구축

OpenTelemetry(오픈텔레메트리)로 Node.js Express·Python FastAPI 서비스에 분산 트레이싱을 도입하는 실전 가이드. @opentelemetry/sdk-node 설치부터 자동 계측, Jaeger 시각화, OTel Collector 배포, Prometheus 메트릭 연동까지 단계별로 정리했다. 커스텀 스팬·속성 추가 코드와 흔한 설정 오류 5가지 트러블슈팅을 포함해 프로덕션 관측가능성 파이프라인을 처음부터 구성할 수 있다.

by
배포 직후 서비스가 느려졌다. 로그를 뒤졌지만 어느 서비스에서 병목이 생겼는지 알 수 없다. DB 쿼리인지, 외부 API 호출인지, JSON 직렬화인지 — 추적이 안 된다. 분산 시스템에서 이 문제를 해결하는 표준 도구가 OpenTelemetry(오픈텔레메트리)다. 오픈텔레메트리는 CNCF가 관리하는 오픈소스 관측가능성 프레임워크다. 트레이스·메트릭·로그 세 가지 신호를 하나의 SDK로 수집하고, 벤더 중립적인 방식으로 Jaeger·Prometheus·Datadog 등 어느 백엔드에도 전송할 수 있다. 이 글에서는 Node.js Express 앱과 Python FastAPI 앱에 오픈텔레메트리를 붙이는 법을 단계별로 정리한다. 로컬 Jaeger 시각화부터 Collector 배포, Prometheus 연동까지 실제로 작동하는 코드만 담는다. 개발 환경 설정에서 시작해 프로덕션 수준의 파이프라인을 갖추는 전 과정을 다룬다.

관측가능성이 없으면 무슨 일이 벌어지나 — 도입 전 현실

관측가능성 도구를 갖추지 않은 팀이 서비스 장애를 경험하면 공통적으로 같은 방식으로 대응한다. 슬랙 채널에 "뭔가 느려요"라는 제보가 올라온다. 서버에 접속해 로그를 뒤진다. 에러 메시지를 grep한다. 관련 있어 보이는 서비스 몇 개를 재시작한다. 그러면서 어디서 문제가 생겼는지 확신 없이 시간을 보낸다. 이런 디버깅 방식의 문제는 재현이 안 된다는 것이다. 스테이징 환경에서는 문제가 안 생기고, 프로덕션에서만 간헐적으로 느려진다. DB 쿼리가 느린 건지, 외부 결제 API 응답이 늦은 건지, 아니면 특정 사용자의 요청만 느린 건지 구분이 안 된다. 오픈텔레메트리를 도입하면 이 과정이 달라진다. 느린 요청이 들어오면 Jaeger에서 해당 트레이스를 찾는다. 어느 스팬에서 시간이 가장 많이 걸렸는지 타임라인으로 한눈에 본다. "외부 PG사 결제 API 호출이 3초 걸렸고, 재시도가 2번 발생했다"는 식으로 정확한 원인을 파악한다. 장애 대응 시간이 몇 시간에서 수 분으로 줄어든다.

오픈텔레메트리 핵심 개념 — 트레이스·메트릭·로그의 삼각형

관측가능성을 구성하는 세 가지 신호를 먼저 정리한다. 트레이스(Trace): 요청 하나가 여러 서비스를 통과하는 전체 경로다. 각 단계를 스팬(Span)이라 부른다. "이 API 요청이 DB에서 얼마나 걸렸나?"를 답한다. 스팬은 부모-자식 관계로 연결되어 전체 호출 트리가 만들어진다. 분산 마이크로서비스에서는 하나의 요청이 5~10개 서비스를 거치는 경우가 많은데, 트레이스 하나로 전체 경로를 볼 수 있다. 메트릭(Metric): 집계된 수치다. 초당 요청 수, 에러율, 레이턴시 P99 등이다. "지금 시스템이 건강한가?"를 답한다. 카운터, 게이지, 히스토그램 세 가지 형태가 있다. Grafana 대시보드로 실시간 추이를 볼 때 사용한다. 로그(Log): 이벤트 기록이다. 구조화된 로그에 트레이스 ID를 심으면 트레이스와 연결된다. "정확히 무슨 일이 일어났나?"를 답한다. 오픈텔레메트리 이전에는 이 세 신호를 수집하는 라이브러리가 벤더마다 달랐다. Jaeger를 쓰면 Jaeger 전용 SDK, Datadog을 쓰면 Datadog 에이전트를 코드에 직접 심어야 했다. 벤더를 바꾸면 코드를 다시 써야 했다. 오픈텔레메트리는 이 문제를 해결한다. 코드에는 오픈텔레메트리 SDK만 심고, 백엔드는 설정 파일에서 바꾸면 된다. 이것이 오픈텔레메트리를 배울 가치가 있는 핵심 이유다.
OpenTelemetry 아키텍처 구성도
오픈텔레메트리 구성 요소: SDK가 신호를 수집하고 Collector가 백엔드로 라우팅한다

OTel 구성 요소 완전 해부 — API, SDK, 계측 라이브러리, Collector

오픈텔레메트리를 처음 접하면 패키지가 너무 많다고 느낀다. 구조를 이해하면 혼란이 사라진다. API 레이어: 계측 인터페이스만 정의한다. 실제 구현이 없어 의존성이 없다. 라이브러리를 만들 때 이 레이어만 의존하면 라이브러리 사용자가 어떤 SDK를 써도 충돌이 없다. SDK 레이어: API의 실제 구현체다. 스팬 생성, 샘플링, 익스포터 관리를 담당한다. 애플리케이션에는 이 레이어를 설치한다. 계측 라이브러리: Express, HTTP, 데이터베이스 드라이버 같은 주요 라이브러리에 자동 계측을 추가한다. 코드 한 줄 없이 HTTP 요청과 응답, DB 쿼리, 외부 API 호출이 스팬으로 기록된다. 기존 코드를 변경하지 않아도 된다는 점이 현업 도입에서 가장 큰 장점이다. OTel Collector: 수신자·처리기·익스포터로 구성된 독립 데몬이다. 여러 소스에서 신호를 받아 변환·필터·샘플링한 뒤 여러 백엔드로 라우팅한다. 개발 환경에서는 없어도 되지만 프로덕션에서는 Collector를 두는 것이 표준이다. 일반적인 구성 패턴은 이렇다: 앱 SDK에서 OTLP 프로토콜로 Collector에 보내고, Collector가 Jaeger(트레이스)와 Prometheus(메트릭)로 라우팅한다. 백엔드를 Datadog으로 교체하고 싶으면 Collector 설정만 변경하고 앱 코드는 그대로 둔다.

Node.js Express 트레이싱 — SDK 설치와 자동 계측 설정

Node.js 프로젝트에 오픈텔레메트리를 붙이는 가장 빠른 방법은 공식 Node.js SDK와 자동 계측 패키지를 사용하는 것이다. Express, HTTP, fetch, PostgreSQL 클라이언트가 코드 변경 없이 자동으로 계측된다. 먼저 필요한 패키지를 설치한다.
npm 패키지 설치
# 기본 SDK와 자동 계측 npm install @opentelemetry/sdk-node npm install @opentelemetry/auto-instrumentations-node # OTLP 익스포터 (gRPC 방식 — Jaeger·Collector 전송용) npm install @opentelemetry/exporter-trace-otlp-grpc npm install @opentelemetry/exporter-metrics-otlp-grpc npm install @grpc/grpc-js
다음으로 계측 설정 파일을 만든다. 이 파일은 앱 진입점보다 먼저 로드해야 계측이 제대로 적용된다.
instrumentation.js — 계측 초기화 파일
// instrumentation.js 'use strict'; const { NodeSDK } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc'); const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); const { Resource } = require('@opentelemetry/resources'); const { SEMRESATTRS_SERVICE_NAME } = require('@opentelemetry/semantic-conventions'); const sdk = new NodeSDK({ resource: new Resource({ [SEMRESATTRS_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'my-service', }), traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317', }), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317', }), exportIntervalMillis: 10_000, // 10초마다 메트릭 전송 }), instrumentations: [ getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-http': { enabled: true }, '@opentelemetry/instrumentation-express': { enabled: true }, '@opentelemetry/instrumentation-pg': { enabled: true }, }), ], }); sdk.start(); process.on('SIGTERM', () => { sdk.shutdown().then(() => process.exit(0)); });
package.json — 계측 파일을 먼저 로드하는 시작 스크립트
{ "scripts": { "start": "node --require ./instrumentation.js src/index.js", "dev": "nodemon --require ./instrumentation.js src/index.js" } }
이 설정만으로 Express의 모든 HTTP 요청, PostgreSQL 쿼리, 외부 fetch 호출이 자동으로 스팬에 기록된다. 기존 코드는 한 줄도 바꾸지 않아도 된다. 환경 변수로 서비스명과 엔드포인트를 관리하는 것이 좋다. 도커 컴포즈나 쿠버네티스 환경에서는 서비스명을 OTEL_SERVICE_NAME 환경변수로 주입하고, Collector 주소를 OTEL_EXPORTER_OTLP_ENDPOINT로 지정한다. 코드에 주소를 하드코딩하지 않으면 환경별(개발·스테이징·프로덕션)로 설정만 바꿔 사용할 수 있다.
Jaeger UI 트레이스 타임라인 화면
Jaeger UI에서 분산 트레이스를 확인하는 화면. 각 스팬의 레이턴시가 타임라인으로 표시된다

Python FastAPI 트레이싱 — opentelemetry-sdk 자동 계측 설정

Python 쪽은 공식 SDK와 프레임워크별 계측 패키지를 사용한다. FastAPI, SQLAlchemy, httpx, requests가 모두 자동 계측된다. Node.js와 개념은 동일하지만 설정 방식이 다르다.
requirements.txt — Python 의존성
opentelemetry-sdk==1.25.0 opentelemetry-instrumentation-fastapi==0.46b0 opentelemetry-instrumentation-httpx==0.46b0 opentelemetry-instrumentation-sqlalchemy==0.46b0 opentelemetry-exporter-otlp-proto-grpc==1.25.0
telemetry.py — FastAPI 계측 초기화
# telemetry.py import os 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 from opentelemetry.sdk.resources import Resource, SERVICE_NAME from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor def setup_telemetry(app): resource = Resource.create({ SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "fastapi-service"), }) exporter = OTLPSpanExporter( endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"), ) provider = TracerProvider(resource=resource) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) FastAPIInstrumentor.instrument_app(app) HTTPXClientInstrumentor().instrument()
main.py — FastAPI 앱 진입점
# main.py from fastapi import FastAPI from telemetry import setup_telemetry app = FastAPI() setup_telemetry(app) @app.get("/items/{item_id}") async def get_item(item_id: int): return {"item_id": item_id, "name": "예제 아이템"}
Python FastAPI에서는 앱 인스턴스를 생성한 뒤 계측 함수를 호출한다. Node.js와 달리 별도 실행 플래그가 필요 없고, 앱 시작 코드 안에서 계측을 초기화하는 방식이다. SQLAlchemy를 사용하는 경우 엔진 객체를 계측 함수에 전달하면 모든 SQL 쿼리가 자동으로 스팬으로 기록된다. PostgreSQL, MySQL, SQLite 모두 지원한다. Django 환경이라면 opentelemetry-instrumentation-django 패키지를 대신 사용한다.

Jaeger 로컬 설치와 트레이스 시각화

Jaeger는 CNCF 졸업 프로젝트로 Uber에서 시작한 분산 트레이싱 시각화 도구다. 로컬 개발에서는 올인원 이미지로 5분 안에 실행할 수 있다. 설치가 간단하고 UI가 직관적이어서 오픈텔레메트리 도입 초기에 가장 많이 사용된다.
docker-compose.yml — 로컬 Jaeger 구성
version: '3.8' services: jaeger: image: jaegertracing/all-in-one:1.56 environment: - COLLECTOR_OTLP_ENABLED=true ports: - "16686:16686" # Jaeger UI - "4317:4317" # OTLP gRPC - "4318:4318" # OTLP HTTP restart: unless-stopped api: build: . environment: - OTEL_SERVICE_NAME=my-express-api - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317 depends_on: - jaeger ports: - "3000:3000"
도커 컴포즈로 실행하면 http://localhost:16686에서 Jaeger UI를 확인할 수 있다. 앱을 실행하고 API를 몇 번 호출한 뒤 Jaeger UI에서 서비스명을 선택하면 트레이스 목록이 표시된다. 각 트레이스를 클릭하면 스팬 타임라인이 펼쳐진다. 어느 DB 쿼리가 느렸는지, 어느 외부 API 호출에서 지연이 생겼는지 막대 그래프로 한눈에 볼 수 있다. 마이크로서비스 환경이라면 여러 서비스를 가로지르는 전체 경로가 하나의 트레이스로 연결되어 표시된다. Jaeger 1.56 버전부터 OTLP를 기본 지원하므로 별도 Collector 없이 앱이 직접 전송할 수 있다. 개발 초기에 빠르게 설정을 확인할 때 유용하다.

커스텀 스팬과 속성 추가 — 비즈니스 맥락을 트레이스에 심는 법

자동 계측으로 HTTP와 DB 레이턴시는 잡힌다. 그러나 "이 사용자의 결제가 어느 단계에서 실패했나?" 같은 비즈니스 맥락은 직접 스팬을 만들어야 한다. 커스텀 스팬이 오픈텔레메트리를 진정으로 활용하는 핵심이다.
Node.js — 커스텀 스팬과 속성 추가
// Node.js 커스텀 스팬 예제 const { trace, SpanStatusCode } = require('@opentelemetry/api'); const tracer = trace.getTracer('payment-service'); async function processPayment(userId, amount) { return tracer.startActiveSpan('payment.process', async (span) => { try { // 비즈니스 속성 추가 span.setAttribute('user.id', userId); span.setAttribute('payment.amount', amount); span.setAttribute('payment.currency', 'KRW'); const result = await chargeCard(userId, amount); span.setAttribute('payment.transaction_id', result.transactionId); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (err) { span.recordException(err); span.setStatus({ code: SpanStatusCode.ERROR, message: err.message }); throw err; } finally { span.end(); } }); }
Python — 커스텀 스팬과 속성
# Python 커스텀 스팬 예제 from opentelemetry import trace from opentelemetry.trace import StatusCode tracer = trace.get_tracer("payment-service") async def process_payment(user_id: str, amount: int): with tracer.start_as_current_span("payment.process") as span: span.set_attribute("user.id", user_id) span.set_attribute("payment.amount", amount) span.set_attribute("payment.currency", "KRW") try: result = await charge_card(user_id, amount) span.set_attribute("payment.transaction_id", result["id"]) span.set_status(StatusCode.OK) return result except Exception as e: span.record_exception(e) span.set_status(StatusCode.ERROR, str(e)) raise
속성 이름은 오픈텔레메트리 시맨틱 컨벤션(Semantic Conventions)을 따르는 것이 좋다. 표준 이름을 쓰면 Jaeger나 Grafana에서 자동으로 인식하고 필터와 검색이 편해진다. 중첩 스팬도 지원한다. 결제 처리 함수 안에서 카드 인증, 잔액 차감, 영수증 발송을 각각 별도 스팬으로 만들면 타임라인에서 계층 구조로 표시된다. 이 방식으로 비즈니스 로직의 어느 단계가 오래 걸리는지 정밀하게 추적할 수 있다. 에러 발생 시 recordException()으로 스택 트레이스를 스팬에 포함시키면 Jaeger에서 에러 상세를 바로 볼 수 있다. 별도로 에러 로그를 찾아다니지 않아도 된다.

OTel Collector 프로덕션 배포 — 백프레셔·배치·샘플링 설정

개발 환경에서는 앱이 Jaeger에 직접 보내도 된다. 프로덕션에서는 Collector를 중간에 두는 것이 좋다. 이유는 세 가지다. 벤더 분리: 앱은 Collector에만 보내고, Collector가 Jaeger·Datadog·New Relic에 라우팅한다. 벤더를 교체해도 앱 재배포가 필요 없다. 실제로 모니터링 도구를 교체하거나 여러 곳에 동시에 보내야 할 때 이 구조의 진가가 발휘된다. 샘플링: 모든 트레이스를 저장하면 저장 비용이 커진다. Collector에서 에러 요청 100% + 정상 요청 10% 같은 꼬리 샘플링을 적용하면 비용을 대폭 줄일 수 있다. 백프레셔 처리: 백엔드가 느릴 때 Collector가 큐를 들고 재시도한다. 앱 서비스는 영향을 받지 않아 안정적이다.
otel-collector-config.yaml — 기본 Collector 설정
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 memory_limiter: check_interval: 1s limit_mib: 512 spike_limit_mib: 128 # 헬스체크 요청은 트레이스에서 제외 filter/drop-health: traces: span: - 'attributes["http.route"] == "/health"' exporters: otlp/jaeger: endpoint: jaeger:4317 tls: insecure: true prometheus: endpoint: "0.0.0.0:8889" service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, filter/drop-health, batch] exporters: [otlp/jaeger] metrics: receivers: [otlp] processors: [memory_limiter, batch] exporters: [prometheus]
메모리 제한 설정은 Collector가 메모리 한도를 초과하면 신호를 버리는 대신 백프레셔를 걸어 앱에 신호 속도를 늦추도록 요청한다. 프로덕션 필수 설정이다. 헬스체크 필터는 쿠버네티스 환경에서 매우 유용하다. 라이브니스·레디니스 프로브가 초당 수십 번 호출되면 트레이스 데이터 대부분이 헬스체크로 채워진다. 이 필터로 제거하면 실제 비즈니스 트레이스만 저장된다. Collector 자체도 도커나 쿠버네티스로 배포한다. 리소스가 충분하다면 Deployment + Service 방식이 가장 간단하다. 대규모 트래픽 환경에서는 노드마다 DaemonSet으로 배치하는 방식을 쓴다.
OTel Collector 파이프라인 구성도
Collector가 여러 소스에서 신호를 받아 처리한 뒤 여러 백엔드로 라우팅하는 구조

Prometheus·Grafana 메트릭 파이프라인 연동하기

Collector에서 Prometheus 포맷으로 메트릭을 내보내면 Prometheus가 스크랩하고, Grafana가 시각화한다. Node.js SDK는 HTTP 서버 처리 시간과 활성 요청 수 같은 기본 메트릭을 자동으로 생성한다. 자동 생성 메트릭만으로는 비즈니스 지표를 볼 수 없다. 아래처럼 커스텀 카운터와 히스토그램을 추가하면 결제 성공률, 결제 처리 시간 분포 같은 서비스별 핵심 지표를 Grafana에서 바로 볼 수 있다.
Node.js — 커스텀 카운터와 히스토그램
// 커스텀 메트릭 예제 (Node.js) const { metrics } = require('@opentelemetry/api'); const meter = metrics.getMeter('payment-service'); // 카운터 — 결제 시도 횟수 누적 const paymentCounter = meter.createCounter('payment.total', { description: '총 결제 시도 횟수', }); // 히스토그램 — P50/P95/P99 레이턴시 분포 const paymentDuration = meter.createHistogram('payment.duration', { description: '결제 처리 시간 (밀리초)', unit: 'ms', }); async function processPayment(userId, amount) { const startTime = Date.now(); try { const result = await chargeCard(userId, amount); paymentCounter.add(1, { 'payment.method': 'card', status: 'success' }); return result; } catch (err) { paymentCounter.add(1, { 'payment.method': 'card', status: 'error' }); throw err; } finally { paymentDuration.record(Date.now() - startTime, { 'payment.method': 'card', }); } }
Grafana에서 Prometheus 데이터소스를 추가하면 이 메트릭으로 대시보드를 만들 수 있다. 분당 결제 성공률, P95 레이턴시 추이, 에러율 변화 같은 지표를 실시간으로 확인할 수 있다. 오픈텔레메트리 커뮤니티가 제공하는 Grafana 대시보드 템플릿(ID: 19268)을 임포트하면 기본 HTTP 서버 메트릭 대시보드를 즉시 사용할 수 있다. 커스텀 패널을 추가할 때는 히스토그램 분위수 함수로 P95 레이턴시를 시각화하는 패턴이 가장 유용하다.

흔한 설정 오류 5가지와 해결 방법

오픈텔레메트리를 처음 붙일 때 자주 막히는 지점을 정리한다. 1. 스팬이 전혀 안 보인다
원인: 계측 파일이 앱보다 늦게 로드됨. instrumentation.js가 express 모듈보다 먼저 로드돼야 한다. Node.js는 --require ./instrumentation.js 플래그를 쓰는 것이 가장 안전하다. express를 가져온 뒤 SDK를 초기화하면 자동 계측이 적용되지 않는다. 2. OTLP 연결 오류 — "No connection established"
원인: Collector나 Jaeger가 아직 준비되지 않았거나 포트가 다름. gRPC 포트는 4317, HTTP 포트는 4318이다. docker compose ps로 상태를 확인한 뒤 재시도한다. 방화벽이나 네트워크 정책이 포트를 막고 있을 수도 있다. 3. Python에서 비동기 스팬이 부모 없이 나온다
원인: 비동기 컨텍스트를 제대로 이어받지 못한 경우다. FastAPI의 비동기 핸들러 안에서도 동기 with문으로 감싸면 컨텍스트가 올바르게 전파된다. 별도 태스크로 분기하는 경우는 컨텍스트를 명시적으로 전달해야 한다. 4. 메트릭이 Prometheus에 안 들어간다
원인: Collector의 Prometheus 익스포터 포트를 Prometheus 스크랩 설정에 추가해야 함. prometheus.yml의 스크랩 설정에 Collector 주소를 추가한다. Grafana 데이터소스도 Prometheus URL을 올바르게 가리키는지 확인한다. 5. 프로덕션에서 메모리 사용량이 급증한다
원인: 스팬 처리기 큐가 넘침. 큐 크기와 전송 주기를 환경에 맞게 조정한다. Collector의 메모리 제한도 함께 설정해야 앱에 백프레셔가 제대로 전달된다. 샘플링 비율을 낮추는 것도 효과적이다.

참고 자료


FAQ — 자주 묻는 질문

오픈텔레메트리와 Jaeger, Zipkin은 어떤 관계인가?

Jaeger와 Zipkin은 분산 트레이싱 백엔드다. 트레이스 데이터를 저장하고 시각화하는 도구다. 오픈텔레메트리는 이 백엔드에 데이터를 보내는 SDK와 프로토콜이다. 예전에는 Jaeger를 쓰면 Jaeger 전용 클라이언트를 코드에 심었다. 이제는 오픈텔레메트리 SDK를 심고, Collector 설정으로 Jaeger·Zipkin·Datadog 어디든 보낼 수 있다. 코드를 바꾸지 않아도 되는 것이 핵심 장점이다.


Collector 없이 앱에서 Jaeger에 직접 보내도 되나?

개발 환경에서는 가능하다. Jaeger 1.56 이상은 OTLP를 직접 수신하므로 별도 Collector 없이 앱이 직접 전송할 수 있다. 그러나 프로덕션에서는 Collector를 두는 것이 좋다. 백엔드를 교체할 때 앱 재배포 없이 Collector 설정만 바꾸면 되고, 샘플링·필터링·변환을 Collector에서 처리할 수 있어 앱 코드가 단순해진다.


샘플링은 어떻게 설정하나? 모든 요청을 저장하면 비용이 크다.

오픈텔레메트리는 두 가지 샘플링을 지원한다. 헤드 샘플링은 요청 시작 시 비율로 결정한다. SDK 설정에서 비율 기반 샘플러를 적용하면 전체 트레이스의 일정 비율만 수집한다. 꼬리 샘플링은 요청이 끝난 뒤 에러 여부나 레이턴시 기준으로 결정한다. Collector에서 처리하며, 에러 요청 100% 저장, 정상 요청 10% 저장이 일반적인 패턴이다.


쿠버네티스 환경에서 OTel Collector를 어떻게 배포하나?

두 가지 방식이 있다. DaemonSet으로 노드마다 Collector를 두거나, Deployment로 클러스터 공유 Collector를 둔다. CNCF가 배포한 OpenTelemetry Operator를 쓰면 커스텀 리소스로 Collector를 선언적으로 관리할 수 있다. 사이드카 모드도 지원해 파드마다 Collector 컨테이너를 자동으로 주입한다. 처음 도입하거나 트래픽이 많지 않으면 Deployment 하나로 시작하는 것이 간단하다.


오픈텔레메트리 도입 후 성능 오버헤드가 걱정된다.

오픈텔레메트리 SDK는 비동기 방식으로 스팬을 처리한다. 스팬 생성과 전송이 별도 스레드에서 이루어져 요청 처리 경로에 영향이 거의 없다. 실제 운영 환경 측정 기준으로 CPU 사용률 증가가 1~3% 수준이다. 샘플링 비율을 낮추면 오버헤드도 함께 줄어든다. 자동 계측 라이브러리 중 불필요한 것은 비활성화하는 것도 방법이다.


로그와 트레이스를 연결하는 방법은?

활성 스팬의 트레이스 ID와 스팬 ID를 로그에 주입하면 된다. Node.js에서는 현재 활성 스팬에서 컨텍스트 정보를 가져와 로거 필드로 추가한다. Python은 현재 스팬 컨텍스트를 가져와 structlog나 logging 핸들러에 추가한다. Grafana Loki와 Tempo를 함께 쓰면 로그 화면에서 해당 트레이스로 직접 이동하는 연결이 지원된다.


OpenTelemetryOTel분산 트레이싱JaegerPrometheusNode.jsPython옵저버빌리티GrafanaCollector

함께 보면 좋은 문제 해결

EXPLORE / Open Source

이어서 읽어보기

전체 토픽 둘러보기