Bỏ qua để đến nội dung
Knowledge Hub

Correlation ID đi qua hai service: nối log đúng cách, đừng nhầm với tracing Trung cấp

Thời gian đọc ước tính: 6 phút

Một request tạo đơn đi qua API rồi gọi service phí. Khi có hàng nghìn request đồng thời, các dòng log của một request nằm lẫn giữa log người khác. Timestamp gần nhau không chứng minh cùng một request, và orderId chưa chắc đã có lúc request bắt đầu.

Correlation ID là định danh gắn với một luồng xử lý để tra các log liên quan. API tạo ID, đưa vào log, gửi qua header khi gọi downstream; downstream ghi lại ID nhận được. Đây là output thật của demo trong repo (một request, hai service, cùng một ID):

{"event":"api.start","requestId":"a0638d3f-f7ea-4ef0-87a6-15734e1f3ad0"}
{"event":"downstream.received","requestId":"a0638d3f-f7ea-4ef0-87a6-15734e1f3ad0"}
{"event":"api.after-downstream","requestId":"a0638d3f-f7ea-4ef0-87a6-15734e1f3ad0"}

Một câu requestId:"a0638d3f..." trong log aggregator ra đủ ba dòng, kể cả khi log nằm ở hai service.

x-request-id traceparent baggage
Mang gì Một ID để tìm log trace-id + parent-id + flags Cặp key-value nghiệp vụ
Chuẩn Quy ước, không phải chuẩn W3C Trace Context (Recommendation 2021) W3C Baggage (Candidate Recommendation)
Ai tạo Ingress/gateway Tracing SDK (OpenTelemetry) Ứng dụng
Cho biết cây span, latency Không Có, kèm span Không
Dùng khi Chưa có tracing Đã có tracing Cần truyền ngữ cảnh như tenant, tier

Quy tắc: đã chạy OpenTelemetry thì dùng traceparent và đưa trace_id vào log, đừng hand-roll thêm header. x-request-id chỉ còn lý do tồn tại ở ingress, như ID trả cho client để support tra cứu.

Định dạng traceparent (W3C): bốn field cách nhau bằng dấu gạch ngang.

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
│ └──── trace-id (32 hex) ───────┘ └─ parent-id ──┘ └ flags
version (00) (16 hex) (01 = sampled)
  • trace-id và parent-id toàn số 0 là không hợp lệ.
  • tracestate đi kèm để mang dữ liệu riêng của từng vendor tracing.
  • baggage có thể chứa dữ liệu nhận dạng người dùng và đi sang mọi hệ thống phía sau; không đưa dữ liệu nhạy cảm vào và lọc ở ranh giới tin cậy.

AsyncLocalStorage giữ store gắn với chuỗi bất đồng bộ trong một process; hàm log() đọc requestId mà không cần truyền tham số. Store đó không tự đi qua HTTP, broker hay service khác: code gọi HTTP phải đặt header, bên nhận đọc và gắn vào context của mình. Chi tiết cơ chế xem AsyncLocalStorage.

Một request ID được đặt vào context nội bộ và chuyển qua HTTP header
Một request ID được đặt vào context nội bộ và chuyển qua HTTP headerBước 1/4: Request vào API. API tạo hoặc xác thực correlation ID ở ranh giới tin cậy.ClientAPIDownstreamrequest
Client → APIrequest
Bước 1/4

Request vào API. API tạo hoặc xác thực correlation ID ở ranh giới tin cậy.

Header từ client là input không đáng tin: có thể rất dài, chứa newline, PII hoặc trùng ID request khác. Edge tạo ID mới, hoặc validate theo regex và giới hạn độ dài; ID của client, nếu cần, lưu ở field riêng.

Message queue không có “request” HTTP, nên ID phải đi trong record header; consumer đọc lại và khôi phục context.

// producer
await producer.send({ topic: 'orders', messages: [{
key: orderId, value: payload,
headers: { traceparent: currentTraceparent(), 'x-request-id': requestId },
}] });
// consumer
const requestId = message.headers?.['x-request-id']?.toString();
context.run({ requestId }, () => handle(message)); // log trong handler có ID

Với OpenTelemetry, các instrumentation HTTP và Kafka client thường inject/extract traceparent tự động; kiểm tra bằng cách chụp header outbound trong integration test.

Dùng Không đủ
Ghép log của một request qua nhiều service Cần latency từng hop: dùng trace
Support tra cứu bằng ID trả trong response Cần theo dõi cả workflow nhiều request: thêm business ID hoặc baggage
Hệ thống chưa có tracing Dùng như idempotency key hoặc quyền truy cập: sai mục đích

Chạy demo từ gốc repo (hai server loopback, không cần Docker):

Terminal window
npx --prefix examples ts-node examples/src/correlation-id/correlation-demo.ts

Thử xóa header trong fetch: log downstream mất ID. Thử bỏ context.run: log API mất ID. Hai cơ chế độc lập. Demo chỉ chứng minh một HTTP hop; chưa có retry, broker hay OpenTelemetry.

  1. Ingress tạo ID mới, hoặc validate ID nhận được (regex, tối đa khoảng 64 ký tự, không newline).
  2. Echo ID trong response header để client và support dùng.
  3. Dùng một tên field log thống nhất (request_id, trace_id) ở mọi service.
  4. Wrap HTTP client và Kafka producer để forward header; consumer khôi phục context.
  5. Không dùng biến global cho ID; dùng AsyncLocalStorage.
  6. Đã có OpenTelemetry thì ưu tiên traceparent, log kèm trace_id và span_id.
  7. Test hai request song song và test downstream timeout vẫn giữ ID trong log lỗi.

Có ích cho

  • Ghép log của một lần xử lý qua các hop mà không đoán theo timestamp.
  • AsyncLocalStorage bỏ việc truyền ID qua mọi chữ ký hàm.
  • Header tường minh dễ test ở ranh giới network.

Giới hạn và chi phí

  • Một service quên forward là đứt chuỗi.
  • Request ID không có span, latency hay quan hệ cha-con.
  • Log vẫn cần retention, masking và kiểm soát truy cập.

Tự kiểm tra: (1) Xóa x-request-id khỏi fetch thì log nào mất ID? (2) Field parent-id của traceparent dài bao nhiêu ký tự hex? (3) Message qua Kafka mang ID bằng cách nào?