관찰 가능성(Observability) 실전 가이드 2026: OpenTelemetry로 블랙박스 시스템 해체하기
로그만으로는 부족한 시대. OpenTelemetry로 메트릭, 로그, 트레이스를 통합하는 실전 가이드. Node.js + Next.js 환경에서 분산 추적을 설정하고 운영 장애를 30분 만에 찾아내는 방법.

요약
로그만으로는 부족한 시대. OpenTelemetry로 메트릭, 로그, 트레이스를 통합하는 실전 가이드. Node.js + Next.js 환경에서 분산 추적을 설정하고 운영 장애를 30분 만에 찾아내는 방법.
관찰 가능성(Observability) 실전 가이드 2026: OpenTelemetry로 블랙박스 시스템 해체하기
Executive Summary - Topic: OpenTelemetry 기반 관찰가능성(Observability) 구축 실전 가이드 - Target: 백엔드 엔지니어, DevOps, SRE, 시니어 개발자 - TL;DR 1: 로그(Log) + 메트릭(Metric) + 트레이스(Trace) 세 가지 신호를 상관관계로 묶는 것이 현대 관찰가능성의 핵심 - TL;DR 2: OpenTelemetry는 벤더 중립 표준 — Datadog, Grafana, Jaeger 중 어디든 전환 가능 - TL;DR 3: 계측(Instrumentation) 없는 로그는 "사건 기록"이고, 계측된 트레이스는 "사건 재현"이다
장애가 발생했을 때 로그 파일을 grep하면서 원인을 찾는 데 4시간이 걸렸다면, 그 시스템은 관찰 가능성(Observability)이 없는 것입니다.
30분 안에 원인을 찾은 팀은 무엇이 달랐을까요? 바로 분산 추적(Distributed Tracing)과 구조화된 메트릭이 있었습니다.
관찰가능성의 3가지 축
1├── Logs (무슨 일이 있었나?)2│ "2026-03-05T09:23:41 ERROR order-service: Payment timeout"3│4├── Metrics (얼마나 자주, 얼마나 많이?)5│ order.payment.duration_ms: p99=2847ms (정상 p99: 450ms)6│7└── Traces (어디서 느려졌나?)8 [order-service 2.85s]9 ├── [auth-service 12ms] ✅10 ├── [inventory-service 23ms] ✅11 ├── [payment-service 2.7s] ← 여기!12 │ ├── [pg-query: SELECT... 2.6s] ← DB 슬로우쿼리13 │ └── [redis: GET 45ms] ✅14 └── [notification-service 38ms] ✅핵심: 세 가지를 단일 Trace ID로 연결해야 합니다. 로그에 Trace ID가 없으면 상관관계 분석이 불가능합니다.
OpenTelemetry란?
OpenTelemetry(OTel)는 CNCF 졸업 프로젝트로, 관찰가능성 신호를 수집하는 벤더 중립 표준입니다.
1[앱 코드 + OTel SDK]2 ↓ (OTLP 프로토콜)3[OTel Collector] (수집/변환/라우팅)4 ↓5[백엔드 선택]6├── Jaeger (트레이스, 오픈소스)7├── Grafana Tempo (트레이스, 오픈소스)8├── Prometheus (메트릭, 오픈소스)9├── Grafana Loki (로그, 오픈소스)10├── Datadog (올인원, 상용)11└── AWS X-Ray / CloudWatch (AWS 환경)장점: OTel로 계측하면 백엔드를 언제든 교체할 수 있습니다. Datadog에서 Grafana Stack으로 전환해도 앱 코드는 그대로입니다.
Node.js 설정
1npm install @opentelemetry/sdk-node \2 @opentelemetry/auto-instrumentations-node \3 @opentelemetry/exporter-otlp-grpc \4 @opentelemetry/resources \5 @opentelemetry/semantic-conventions1// instrumentation.js (앱 시작 전에 실행)2import { NodeSDK } from '@opentelemetry/sdk-node';3import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';4import { OTLPTraceExporter } from '@opentelemetry/exporter-otlp-grpc';5import { Resource } from '@opentelemetry/resources';6import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';7 8const sdk = new NodeSDK({9 resource: new Resource({10 [SemanticResourceAttributes.SERVICE_NAME]: 'order-service',11 [SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION || '1.0.0',12 'deployment.environment': process.env.NODE_ENV || 'development',13 }),14 traceExporter: new OTLPTraceExporter({15 url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',16 }),17 instrumentations: [18 getNodeAutoInstrumentations({19 // HTTP 요청 자동 계측20 '@opentelemetry/instrumentation-http': {21 ignoreIncomingRequestHook: (req) => {22 // 헬스체크는 추적 제외23 return req.url === '/health' || req.url === '/metrics';24 },25 },26 // PostgreSQL 자동 계측27 '@opentelemetry/instrumentation-pg': { enabled: true },28 // Redis 자동 계측29 '@opentelemetry/instrumentation-ioredis': { enabled: true },30 }),31 ],32});33 34sdk.start();35 36process.on('SIGTERM', () => sdk.shutdown());1# 실행 시 계측 파일 먼저 로드2node --require ./instrumentation.js server.jsNext.js 설정
1// instrumentation.ts (Next.js 14+에서 자동 로드)2export async function register() {3 if (process.env.NEXT_RUNTIME === 'nodejs') {4 const { NodeSDK } = await import('@opentelemetry/sdk-node');5 const { OTLPTraceExporter } = await import('@opentelemetry/exporter-otlp-http');6 7 const sdk = new NodeSDK({8 resource: new Resource({9 'service.name': 'timeware-web',10 'service.version': process.env.npm_package_version,11 }),12 traceExporter: new OTLPTraceExporter({13 url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,14 }),15 });16 17 sdk.start();18 }19}1// next.config.js2module.exports = {3 experimental: {4 instrumentationHook: true, // instrumentation.ts 활성화5 },6};커스텀 스팬과 속성 추가
자동 계측만으로는 부족한 경우 비즈니스 로직에 직접 스팬을 추가합니다:
1import { trace, context, SpanStatusCode } from '@opentelemetry/api';2 3const tracer = trace.getTracer('order-service', '1.0.0');4 5async function processOrder(orderId: string, userId: string) {6 return tracer.startActiveSpan('order.process', async (span) => {7 try {8 // 비즈니스 컨텍스트 속성 추가9 span.setAttributes({10 'order.id': orderId,11 'user.id': userId,12 'order.type': 'online',13 });14 15 const inventory = await checkInventory(orderId);16 span.addEvent('inventory.checked', { available: inventory.available });17 18 if (!inventory.available) {19 span.setStatus({20 code: SpanStatusCode.ERROR,21 message: 'Inventory not available',22 });23 span.recordException(new Error('Inventory not available'));24 throw new Error('Out of stock');25 }26 27 const payment = await processPayment(orderId, userId);28 span.addEvent('payment.processed', {29 'payment.method': payment.method,30 'payment.amount': payment.amount,31 });32 33 span.setStatus({ code: SpanStatusCode.OK });34 return { orderId, paymentId: payment.id };35 36 } catch (error) {37 span.recordException(error as Error);38 span.setStatus({39 code: SpanStatusCode.ERROR,40 message: (error as Error).message,41 });42 throw error;43 } finally {44 span.end();45 }46 });47}로그에 Trace ID 주입
트레이스와 로그를 연결하는 핵심 설정:
1import { trace, context } from '@opentelemetry/api';2import pino from 'pino';3 4const logger = pino({5 formatters: {6 log: (object) => {7 const span = trace.getActiveSpan();8 if (span) {9 const spanContext = span.spanContext();10 return {11 ...object,12 'trace.id': spanContext.traceId, // Trace ID 주입13 'span.id': spanContext.spanId, // Span ID 주입14 };15 }16 return object;17 },18 },19});20 21// 이제 로그에서 Trace ID로 트레이스를 바로 조회 가능22logger.error({ orderId: '12345', error: err }, 'Payment failed');23// → {"trace.id": "4bf92f3577b34da6...", "span.id": "00f067aa0ba902b7", "orderId": "12345", ...}오픈소스 관찰가능성 스택 (비용 $0)
1# docker-compose.yml (개발/소규모 운영)2version: '3.8'3services:4 otel-collector:5 image: otel/opentelemetry-collector-contrib:latest6 ports:7 - "4317:4317" # OTLP gRPC8 - "4318:4318" # OTLP HTTP9 volumes:10 - ./otel-collector-config.yml:/etc/otel/config.yml11 command: ["--config=/etc/otel/config.yml"]12 13 jaeger: # 트레이스 백엔드14 image: jaegertracing/all-in-one:1.5515 ports:16 - "16686:16686" # UI17 - "14250:14250" # OTLP gRPC18 19 prometheus: # 메트릭 백엔드20 image: prom/prometheus:latest21 ports:22 - "9090:9090"23 volumes:24 - ./prometheus.yml:/etc/prometheus/prometheus.yml25 26 grafana: # 시각화27 image: grafana/grafana:latest28 ports:29 - "3000:3000"30 environment:31 - GF_AUTH_ANONYMOUS_ENABLED=true1# otel-collector-config.yml2receivers:3 otlp:4 protocols:5 grpc:6 endpoint: 0.0.0.0:43177 http:8 endpoint: 0.0.0.0:43189 10exporters:11 jaeger:12 endpoint: jaeger:1425013 tls:14 insecure: true15 prometheus:16 endpoint: "0.0.0.0:8889"17 logging:18 loglevel: warn19 20service:21 pipelines:22 traces:23 receivers: [otlp]24 exporters: [jaeger, logging]25 metrics:26 receivers: [otlp]27 exporters: [prometheus]상용 vs 오픈소스 비교
| 기준 | Grafana Stack (OSS) | Datadog | New Relic |
|---|---|---|---|
| 비용 | 인프라 비용만 | $15~$31/host/월 | $25~$49/host/월 |
| 설정 복잡도 | 높음 | 낮음 | 중간 |
| 기능 완성도 | 충분 (9/10) | 매우 높음 (10/10) | 높음 (9/10) |
| AI 이상 감지 | 제한적 | 강력 | 강력 |
| 데이터 주권 | 완전 (자체 호스트) | 제한적 | 제한적 |
| 규모 | 중소 ~ 중대형 | 대형 | 대형 |
추천:
- 스타트업/소규모: Grafana OSS Stack (Prometheus + Tempo + Loki)
- 중견기업 (SLA 중요): Datadog
- AWS 환경: AWS X-Ray + CloudWatch Container Insights
FAQ
Q. OpenTelemetry를 추가하면 성능에 영향이 있나요? A. 일반적으로 CPU 1~3%, 메모리 20~50MB 추가 사용이 발생합니다. 샘플링 비율을 조정해 트레이드오프를 관리할 수 있습니다. 프로덕션에서는 1~10% 샘플링이 일반적입니다 (에러는 100% 수집).
Q. 기존 로깅 라이브러리(winston, pino)와 OpenTelemetry 로그 수집은 어떻게 통합하나요? A. 두 가지 방법이 있습니다: 1) 로그에 Trace ID를 주입해 Grafana Loki에서 트레이스와 상관관계 분석, 2) OpenTelemetry Log SDK를 사용해 로그 자체를 OTLP로 전송. 대부분의 팀은 방법 1을 먼저 적용합니다 (기존 로깅 코드 변경 없음).
Q. Sampling 전략은 어떻게 결정하나요? A. Head-based sampling(요청 시작 시 결정)과 Tail-based sampling(완료 후 결정)이 있습니다. 에러나 느린 요청을 놓치지 않으려면 Tail-based sampling을 사용하세요. OTel Collector의 tail_sampling 프로세서가 이를 지원합니다. 에러: 100%, 5xx: 100%, 느린 요청(>2s): 100%, 나머지: 5% 설정이 일반적입니다.