Node.js
최종 수정: 2026. 1. 16.
Node.js 계측
Node.js 애플리케이션에 OpenTelemetry를 적용하는 방법을 안내합니다.
개요
Node.js는 OpenTelemetry에서 잘 지원되는 언어입니다.
계측 옵션
| 방식 | 설명 | 코드 변경 |
|---|---|---|
| APM Agent (eBPF) | 커널 레벨 자동 감지 | 불필요 |
| OTel Node SDK | 자동 계측 | 최소 |
| SDK 수동 계측 | 코드에 직접 추가 | 필요 |
자동 계측 (권장)
OTel Operator 사용
가장 쉬운 방법은 OTel Operator를 통한 자동 주입입니다.
Deployment 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-node-app
namespace: production
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-nodejs: "skuber-observability/otel-instrumentation"
spec:
containers:
- name: app
image: my-node-app:1.0.0
ports:
- containerPort: 3000지원 프레임워크
자동으로 계측되는 프레임워크:
| 카테고리 | 프레임워크 |
|---|---|
| 웹 | Express, Koa, Fastify, NestJS, Hapi |
| DB | pg, mysql2, mongodb, mongoose, redis |
| HTTP 클라이언트 | http/https, axios, node-fetch |
| 메시징 | kafkajs, amqplib (RabbitMQ) |
| gRPC | @grpc/grpc-js |
| GraphQL | graphql, @apollo/server |
수동 Agent 설정
Kubernetes 외부 환경에서:
# 패키지 설치
npm install @opentelemetry/auto-instrumentations-node \
@opentelemetry/sdk-node \
@opentelemetry/exporter-trace-otlp-grpctracing.js 파일 생성
// tracing.js - 애플리케이션 시작 전에 로드되어야 함
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-node-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4317',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
// 종료 시 정리
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});애플리케이션 시작
# tracing.js를 먼저 로드
node --require ./tracing.js app.jsDockerfile 예시
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV OTEL_SERVICE_NAME=my-node-app
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
# tracing.js를 먼저 로드하여 실행
CMD ["node", "--require", "./tracing.js", "app.js"]수동 계측
비즈니스 로직에 커스텀 스팬을 추가하려면 수동 계측을 사용합니다.
의존성 설치
npm install @opentelemetry/api \
@opentelemetry/sdk-trace-node \
@opentelemetry/exporter-trace-otlp-grpcSDK 초기화
const opentelemetry = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
function initTracer() {
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'order-service',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
});
const exporter = new OTLPTraceExporter({
url: 'http://otel-collector:4317',
});
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
return opentelemetry.trace.getTracer('order-service');
}
module.exports = { initTracer };트레이스 생성
기본 스팬 생성
const opentelemetry = require('@opentelemetry/api');
const { SpanStatusCode } = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('order-service');
async function createOrder(customerId, items) {
// 스팬 시작
return tracer.startActiveSpan('createOrder', async (span) => {
try {
// 속성 추가
span.setAttribute('customer.id', customerId);
span.setAttribute('items.count', items.length);
// 비즈니스 로직
const order = await processOrder(customerId, items);
// 결과 속성 추가
span.setAttribute('order.id', order.id);
span.setAttribute('order.total', order.total);
return order;
} catch (error) {
// 에러 기록
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}중첩 스팬
async function processOrder(customerId, items) {
return tracer.startActiveSpan('processOrder', async (span) => {
try {
// 하위 스팬은 자동으로 부모와 연결됨
await validateInventory(items);
const pricing = await calculatePricing(items);
const order = await saveOrder(customerId, items, pricing);
return order;
} finally {
span.end();
}
});
}
async function validateInventory(items) {
return tracer.startActiveSpan('validateInventory', async (span) => {
try {
span.setAttribute('items.count', items.length);
// 재고 확인 로직
for (const item of items) {
await checkStock(item);
}
} finally {
span.end();
}
});
}래퍼 함수
function withTracing(name, fn) {
return async (...args) => {
return tracer.startActiveSpan(name, async (span) => {
try {
const result = await fn(...args);
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
};
}
// 사용 예시
const calculatePricing = withTracing('calculatePricing', async (items) => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
});메트릭 생성
const { MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc');
function initMetrics() {
const exporter = new OTLPMetricExporter({
url: 'http://otel-collector:4317',
});
const meterProvider = new MeterProvider({
readers: [
new PeriodicExportingMetricReader({
exporter,
exportIntervalMillis: 60000,
}),
],
});
return meterProvider.getMeter('order-service');
}
// 메트릭 사용
const meter = initMetrics();
const ordersCreated = meter.createCounter('orders.created', {
description: 'Number of orders created',
});
const orderValue = meter.createHistogram('order.value', {
description: 'Order value distribution',
unit: 'USD',
});
// 메트릭 기록
ordersCreated.add(1, { 'customer.type': 'premium' });
orderValue.record(150.50, { currency: 'USD' });프레임워크별 통합
Express
const express = require('express');
const { trace } = require('@opentelemetry/api');
const app = express();
const tracer = trace.getTracer('express-app');
app.post('/orders', async (req, res) => {
await tracer.startActiveSpan('handleOrderRequest', async (span) => {
try {
span.setAttribute('customer.id', req.body.customerId);
const order = await createOrder(req.body);
span.setAttribute('order.id', order.id);
res.json(order);
} catch (error) {
span.recordException(error);
res.status(500).json({ error: error.message });
} finally {
span.end();
}
});
});NestJS
import { Injectable } from '@nestjs/common';
import { trace, SpanStatusCode } from '@opentelemetry/api';
@Injectable()
export class OrderService {
private readonly tracer = trace.getTracer('order-service');
async createOrder(dto: CreateOrderDto): Promise<Order> {
return this.tracer.startActiveSpan('createOrder', async (span) => {
try {
span.setAttribute('customer.id', dto.customerId);
const order = await this.processOrder(dto);
span.setAttribute('order.id', order.id);
return order;
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}
}Fastify
const fastify = require('fastify')();
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('fastify-app');
fastify.post('/orders', async (request, reply) => {
return tracer.startActiveSpan('handleOrderRequest', async (span) => {
try {
span.setAttribute('customer.id', request.body.customerId);
const order = await createOrder(request.body);
return order;
} finally {
span.end();
}
});
});비동기 코드 계측
Promise
async function processItems(items) {
return tracer.startActiveSpan('processItems', async (span) => {
try {
span.setAttribute('items.count', items.length);
// 병렬 처리
const results = await Promise.all(
items.map(item => processItem(item))
);
return results;
} finally {
span.end();
}
});
}컨텍스트 전파
const { context, propagation } = require('@opentelemetry/api');
// HTTP 요청에 컨텍스트 주입
async function callExternalService(data) {
const headers = {};
propagation.inject(context.active(), headers);
const response = await fetch('http://external-service/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers, // 트레이스 컨텍스트 헤더 포함
},
body: JSON.stringify(data),
});
return response.json();
}환경 변수 설정
Kubernetes에서 환경 변수로 설정:
env:
- name: OTEL_SERVICE_NAME
value: "order-service"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector:4317"
- name: OTEL_TRACES_SAMPLER
value: "parentbased_traceidratio"
- name: OTEL_TRACES_SAMPLER_ARG
value: "0.1" # 10% 샘플링
- name: OTEL_LOG_LEVEL
value: "info"트러블슈팅
트레이스가 수집되지 않음
SDK 초기화 순서 확인 (가장 먼저 로드되어야 함):
node --require ./tracing.js app.js의존성 버전 확인:
npm ls | grep opentelemetry디버그 로깅 활성화:
OTEL_LOG_LEVEL=debug node --require ./tracing.js app.js
스팬이 연결되지 않음
startActiveSpan사용 확인- 비동기 코드에서 컨텍스트 전달 확인
span.end()호출 확인
다음 단계
- 자동 계측 - OTel Operator 상세 설정
- 트레이스 - 트레이스 분석 방법
- 서비스 - 서비스 성능 모니터링