Hướng dẫn tích hợp OpenTelemetry vào Node.js: Thiết lập Tracing, Metrics và Logs cho Microservices

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Bối cảnh & Tại sao cần OpenTelemetry trong Microservices

Khi ứng dụng Node.js còn chạy ở dạng monolithic, việc debug lỗi hay đo hiệu năng khá nhàn. Bạn chỉ cần xem file log tập trung hoặc gắn một APM agent cơ bản là thấy ngay call stack. Nhưng khi xé nhỏ hệ thống thành 5–10 microservices chạy trong Docker container hay Kubernetes pod, mọi chuyện sẽ đổi khác hoàn toàn.

Hãy hình dung một kịch bản quen thuộc: người dùng bấm nút “Thanh toán”. Request này đi qua API Gateway, sang User Service để xác thực JWT (mất 15ms), ghé Order Service tạo đơn hàng, gọi qua Payment Service trừ tiền thẻ, rồi mới bắn event sang Notification Service qua Kafka. Đột nhiên một mắt xích nghẽn 2.5 giây hoặc văng lỗi 500.

Lúc này, bạn lần vết từ đâu? SSH vào từng pod để grep file log theo timestamp? Cách này vừa tốn cả tiếng đồng hồ, vừa rất dễ bỏ sót bối cảnh giữa các tiến trình bất đồng bộ.

Vấn đề cốt lõi nằm ở context propagation (lan truyền ngữ cảnh). Các service sinh log rời rạc, format mỗi nơi một kiểu. Hơn nữa, hệ thống không hề có một mã trace_id thống nhất xuyên suốt hành trình request.

Khi hệ thống phình to lên hàng chục service xử lý 2.000–5.000 req/giây, nếu thiếu giải pháp giám sát tập trung, việc tìm root cause sẽ cực kỳ bế tắc. Đó là lý do OpenTelemetry (OTel) trở thành tiêu chuẩn công nghiệp. Được CNCF bảo trợ, OTel cung cấp bộ API và SDK duy nhất để thu thập đủ 3 trụ cột Observability: Traces, Metrics và Logs. Điểm cộng lớn nhất: bạn hoàn toàn tự do đổi backend (Jaeger, Grafana Tempo, Datadog) mà không lo dính chặt vào bất kỳ vendor nào.

Cài đặt các package OpenTelemetry cho Node.js

Hệ sinh thái OpenTelemetry cho Node.js được module hóa rất linh hoạt. Bạn không cần can thiệp thủ công vào từng route Express hay từng câu query PostgreSQL. Gói auto-instrumentation sẽ tự động “hook” vào các thư viện phổ biến như Express, Fastify, HTTP, pg, Redis hay Mongoose.

Hãy mở terminal trong thư mục dự án Node.js và cài đặt các thư viện lõi cùng exporter chuẩn OTLP (OpenTelemetry Protocol):

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

Vai trò cụ thể của từng package:

  • @opentelemetry/sdk-node: Bộ SDK chính quản lý vòng đời và cấu hình thu thập dữ liệu trong tiến trình Node.js.
  • @opentelemetry/auto-instrumentations-node: Bộ plugin tự động bắt trace cho hầu hết thư viện phổ biến mà không cần sửa logic code.
  • @opentelemetry/exporter-*-otlp-http: Đẩy dữ liệu trace và metrics qua giao thức HTTP đến OTel Collector hoặc backend lưu trữ.
  • @opentelemetry/resources & semantic-conventions: Gắn metadata định danh service gồm service name, version và môi trường chạy (production/staging).

Cấu hình chi tiết: Tracing, Metrics và Logs

Một nguyên tắc bất di bất dịch: File khởi tạo tracing phải chạy trước toàn bộ mã nguồn ứng dụng. Nếu bạn require Express hay Mongoose trước khi SDK nạp vào memory, các monkey-patch sẽ mất tác dụng.

1. Thiết lập Distributed Tracing (tracer.js)

Tạo file tracer.js ở thư mục gốc của dự án:

// tracer.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');

const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
});

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.SERVICE_NAME || 'order-service',
    [ATTR_SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter,
  instrumentations: [
    getNodeAutoInstrumentations({
      // Tắt bớt instrumentation fs để tránh sinh quá nhiều span rác và tiết kiệm RAM
      '@opentelemetry/instrumentation-fs': { enabled: false },
    }),
  ],
});

sdk.start();

// Graceful shutdown khi container hoặc tiến trình dừng lại
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry SDK đã dừng an toàn'))
    .catch((err) => console.error('Lỗi khi dừng OTel SDK', err))
    .finally(() => process.exit(0));
});

2. Bổ sung Metrics đo lường hiệu năng hệ thống

Traces đi sâu vào chi tiết từng request, còn metrics đem lại bức tranh toàn cảnh: throughput (RPS), tỷ lệ lỗi 5xx và độ trễ P95/P99. Bạn chỉ cần gắn thêm MetricReader vào cấu hình NodeSDK:

const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');

const metricExporter = new OTLPMetricExporter({
  url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || 'http://localhost:4318/v1/metrics',
});

const metricReader = new PeriodicExportingMetricReader({
  exporter: metricExporter,
  exportIntervalMillis: 15000, // Đẩy dữ liệu định kỳ mỗi 15 giây
});

// Bổ sung metricReader vào options của NodeSDK trong tracer.js:
// const sdk = new NodeSDK({
//   ...
//   metricReader,
// });

Với metrics nghiệp vụ riêng biệt như đếm số đơn hàng thành công, bạn gọi trực tiếp Metrics API:

const { metrics } = require('@opentelemetry/api');

const meter = metrics.getMeter('order-meter');
const orderCounter = meter.createCounter('orders_completed_total', {
  description: 'Tổng số đơn hàng hoàn tất',
});

// Tăng biến đếm khi đơn hàng xử lý thành công
orderCounter.add(1, { payment_method: 'credit_card', status: 'success' });

3. Chuẩn hóa Logs đồng bộ với Trace ID

Lợi ích lớn nhất khi liên kết dữ liệu là khả năng trace-log correlation. Khi lướt log lỗi, bạn copy trace_id và dán vào Grafana hoặc Jaeger. Ngay lập tức, bạn có toàn bộ waterfall chart của request đó.

Với thư viện winston, ta trích xuất span context hiện tại vào từng dòng log:

// logger.js
const { trace, context } = require('@opentelemetry/api');
const winston = require('winston');

const otelFormat = winston.format((info) => {
  const currentSpan = trace.getSpan(context.active());
  if (currentSpan) {
    const spanContext = currentSpan.spanContext();
    info.trace_id = spanContext.traceId;
    info.span_id = spanContext.spanId;
  }
  return info;
});

const logger = winston.createLogger({
  format: winston.format.combine(
    otelFormat(),
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [new winston.transports.Console()],
});

module.exports = logger;

Cấu trúc JSON in ra terminal sẽ kèm theo định danh truy vết đồng nhất:

{
  "level": "info",
  "message": "Tạo đơn hàng thành công",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "timestamp": "2026-10-30T10:15:30.123Z"
}

Kiểm tra & Monitoring thực tế

Cách kiểm chứng nhanh nhất tại local là dựng một cụm Jaeger all-in-one bằng Docker Compose.

Tạo file docker-compose.yml:

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Giao diện web Jaeger UI
      - "4318:4318"   # Cổng nhận OTLP HTTP

Khởi chạy container Jaeger:

docker compose up -d

Khởi động app Node.js kèm cờ --require để nạp file cấu hình OTel trước tiên:

node --require ./tracer.js server.js

Gửi vài request test qua cURL hoặc Postman, sau đó mở trình duyệt tại http://localhost:16686:

  1. Ở mục Service, chọn order-service.
  2. Nhấn Find Traces để tải danh sách request vừa ghi nhận.
  3. Nhấp vào một trace: Jaeger sẽ hiện chi tiết độ trễ từng middleware, thời gian chờ query database và các cuộc gọi HTTP ra bên ngoài.

Trên production với lưu lượng lớn (hàng nghìn req/giây), bạn nên bật sampling (ví dụ: lấy mẫu 10% request) qua TraceIdRatioBasedSampler. Cách này vừa giữ CPU overhead dưới mức 2%, vừa tránh quá tải băng thông mạng và giảm chi phí lưu trữ backend.

Share: