OpenTelemetry Learning
Ngôn ngữ và framework

JavaScript và Node.js

Thiết lập OpenTelemetry cho Node.js với auto-instrumentation, manual spans, OTLP và lifecycle an toàn.

Khởi tạo trước application imports

Instrumentation phải patch module trước khi HTTP, Express hoặc database library được import. Hãy nạp file SDK bằng --require/--import, hoặc import nó ở dòng đầu entrypoint.

Mục lục

Chọn auto hay manual

@opentelemetry/auto-instrumentations-node gom instrumentation cho các module Node.js phổ biến. Dùng nó để tạo HTTP, Express và client spans mà không bọc từng handler. Manual API phù hợp với operation nghiệp vụ mà framework không biết, chẳng hạn orders.validate.

Zero-code launcher của hệ sinh thái tiện cho thử nghiệm. Cấu hình NodeSDK trong một file riêng phù hợp hơn khi cần chọn instrumentation, resource, sampler hoặc exporter bằng code. Một service có thể dùng cả hai, nhưng chỉ nên có một SDK/provider owner.

Cài dependencies

npm install express @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-metrics @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-proto @opentelemetry/exporter-metrics-otlp-proto @opentelemetry/resources @opentelemetry/semantic-conventions

Ví dụ dùng JavaScript CommonJS để giảm cấu hình build. Tạo hai file instrumentation.cjs và app.cjs.

Cấu hình SDK và OTLP

Khởi tạo NodeSDK

// instrumentation.cjs
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-proto');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
  resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'node-orders' }),
  traceExporter: new OTLPTraceExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
    exportIntervalMillis: 10_000,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
module.exports = { sdk };

NodeSDK tạo TracerProvider/MeterProvider, processors/readers và đăng ký chúng làm global providers. Resource mô tả thực thể phát telemetry; tối thiểu cần một service.name ổn định. Exporter đọc OTLP endpoint từ biến môi trường.

CommonJS và ESM

CommonJS có thể preload bằng:

node --require ./instrumentation.cjs app.cjs

Với ESM, dùng bootstrap ESM và cơ chế --import được phiên bản Node đang chạy hỗ trợ. Khả năng hook module ESM phụ thuộc Node, loader và instrumentation package. Luôn kiểm tra compatibility matrix chính thức; nếu một ESM package được import trước SDK, patching có thể không xảy ra.

Thứ tự import là một phần cấu hình

Đừng import app từ cuối file SDK rồi đồng thời preload file đó ở test khác. Giữ bootstrap chỉ làm telemetry, và để Node nạp entrypoint sau khi bootstrap hoàn tất.

Service HTTP và Express

// app.cjs
const express = require('express');
const { trace, metrics, SpanStatusCode } = require('@opentelemetry/api');

const app = express();
const tracer = trace.getTracer('orders', '1.0.0');
const meter = metrics.getMeter('orders', '1.0.0');
const orders = meter.createCounter('orders.created', { unit: '{order}' });

app.get('/orders/:id', async (req, res) => {
  await tracer.startActiveSpan('orders.load', async (span) => {
    try {
      await new Promise((resolve) => setTimeout(resolve, 20));
      span.setAttribute('order.found', true);
      orders.add(1, { 'order.channel': 'http' });
      res.json({ id: req.params.id, status: 'ready' });
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      res.status(500).json({ error: 'internal' });
    } finally {
      span.end();
    }
  });
});

const server = app.listen(3000, () => console.log('listening on :3000'));

Auto-instrumentation sở hữu server span cho route. orders.load là child business span vì startActiveSpan đưa span vào active context trong callback. Không dùng req.params.id làm metric attribute: giá trị gần như vô hạn sẽ tạo cardinality cao.

Context propagation và async

Node SDK dùng context manager dựa trên AsyncLocalStorage để giữ active context qua Promise, timer và phần lớn callback chuẩn. Instrumentation HTTP tự inject header W3C Trace Context (traceparent) khi gọi outbound HTTP và extract nó ở server nhận.

Detached work, worker threads và custom queues là boundary cần xử lý rõ. Với queue, inject context vào message metadata lúc publish và extract lúc consume. Với worker thread, truyền carrier đã serialize; đừng truyền đối tượng Context trực tiếp. Nếu callback library làm mất context, capture context bằng context.active() và chạy callback qua context.with(captured, fn).

Metrics và logs

Metrics API hiện ổn định trong JS SDK. Dùng counter cho số lần, histogram cho phân bố latency/size và observable gauge cho giá trị được quan sát. PeriodicExportingMetricReader ở trên export định kỳ; production nên điều chỉnh interval và views theo backend.

Logs SDK và auto log instrumentation có trạng thái/coverage thay đổi theo package. Nếu chưa dùng OpenTelemetry Logs, tiếp tục ghi structured logs bằng Pino/Winston và cấu hình integration chính thức để thêm trace_id/span_id. Không tự đọc private SDK fields. Xác nhận trạng thái package trước khi đưa log pipeline vào production.

Chạy và xác minh

export OTEL_SERVICE_NAME=node-orders
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=dev
node --require ./instrumentation.cjs app.cjs
curl http://localhost:3000/orders/42

Trong backend, tìm service node-orders. Một request phải có HTTP server span và child orders.load; metric orders.created xuất hiện sau export interval. Nếu dùng Collector, bật debug exporter tạm thời để phân biệt lỗi receiver với lỗi backend.

Shutdown và flush

Đăng ký shutdown ở entrypoint sở hữu process, không ở library:

const { sdk } = require('./instrumentation.cjs');
let stopping = false;

function withTimeout(task, timeoutMs) {
  let timer;
  return Promise.race([
    task,
    new Promise((_, reject) => {
      timer = setTimeout(() => reject(new Error('shutdown timeout')), timeoutMs);
    }),
  ]).finally(() => clearTimeout(timer));
}

function closeServer() {
  return new Promise((resolve, reject) => {
    server.close((error) => error ? reject(error) : resolve());
  });
}

async function stop(signal) {
  if (stopping) return;
  stopping = true;
  const failures = [];

  try {
    await withTimeout(closeServer(), 8_000); // ngừng nhận và drain request
  } catch (error) {
    failures.push(error);
    server.closeAllConnections?.(); // đóng cưỡng bức connection còn treo
  }

  try {
    await withTimeout(sdk.shutdown(), 2_000); // luôn thử flush telemetry
  } catch (error) {
    failures.push(error);
  }

  if (failures.length > 0) {
    process.exitCode = 1;
    console.error('graceful shutdown failed', { signal, failures });
  }
}
process.once('SIGTERM', () => void stop('SIGTERM'));
process.once('SIGINT', () => void stop('SIGINT'));

Đóng HTTP server trước khi gọi sdk.shutdown() để request đang chạy vẫn có thể kết thúc span. Deadline phải nhỏ hơn termination grace period của orchestrator. Đặt process.exitCode thay vì gọi process.exit(0) ngay lập tức để Node có cơ hội hoàn tất callback cleanup; không gọi shutdown sau mỗi request.

Kiểm thử

Unit test business instrumentation bằng in-memory span exporter và SimpleSpanProcessor; reset global providers giữa test suites. Assert span name, parent, status và allowlisted attributes thay vì snapshot toàn bộ span. Integration test nên chạy Collector container, gửi request thật và chờ hữu hạn vì batch export bất đồng bộ.

Một test quan trọng là preload: import app sau bootstrap và xác nhận có server span. Test thêm trường hợp outbound HTTP để chứng minh trace ID được giữ qua propagation.

Lỗi thường gặp

Triệu chứngNguyên nhân thường gặpCách xử lý
Không có HTTP spanExpress/HTTP được import trước SDKPreload bootstrap trước entrypoint
Chỉ có manual spanAuto-instrumentation bị tắt hoặc module không tương thíchBật diagnostic logs và kiểm tra supported versions
Span không cùng traceDetached callback/worker/custom carrierCapture hoặc inject/extract context rõ ràng
ECONNREFUSED khi exportSai cổng/protocol CollectorGhép 4318 với http/protobuf, 4317 với grpc
Process không thoátTimer metric/exporter chưa shutdownGọi sdk.shutdown() trong teardown
Duplicate spansHai SDK hoặc hai lần registrationChỉ định một bootstrap owner

Thực hành production

Dùng batch processing mặc định, sampling có chủ đích và Collector gần workload. Lọc instrumentation không cần thiết để giảm overhead. Không thu thập request body, authorization header hoặc raw SQL nếu chưa có policy. Pin dependencies bằng lockfile và chạy load test khi nâng Node, framework hoặc instrumentation packages.

Đặt service.name, namespace, version và environment nhất quán. Theo dõi exporter failures nhưng không để lỗi telemetry làm hỏng request. Traces/metrics APIs cốt lõi ổn định; coverage instrumentation, ESM hooks và logs vẫn phụ thuộc phiên bản hệ sinh thái.

Bước tiếp theo

On this page