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-grpc

tracing.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.js

Dockerfile 예시

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-grpc

SDK 초기화

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"

트러블슈팅

트레이스가 수집되지 않음

  1. SDK 초기화 순서 확인 (가장 먼저 로드되어야 함):

    node --require ./tracing.js app.js
  2. 의존성 버전 확인:

    npm ls | grep opentelemetry
  3. 디버그 로깅 활성화:

    OTEL_LOG_LEVEL=debug node --require ./tracing.js app.js

스팬이 연결되지 않음

  1. startActiveSpan 사용 확인
  2. 비동기 코드에서 컨텍스트 전달 확인
  3. span.end() 호출 확인

다음 단계

  • 자동 계측 - OTel Operator 상세 설정
  • 트레이스 - 트레이스 분석 방법
  • 서비스 - 서비스 성능 모니터링