OpenTelemetry Tracing — Toàn tập: Node.js/TypeScript, Collector và Jaeger phần 4
Nếu job bị delay lâu hoặc retry nhiều lần, chuyển sang Link: lấy span context bằng trace.getSpanContext(propagation.extract(ROOT_CONTEXT, carrier)) rồi truyền vào links của startActiveSpan thay vì dùng làm parent — trace của producer không bị kéo dài theo thời gian chờ hàng đợi.
gRPC
@opentelemetry/instrumentation-grpc bọc @grpc/grpc-js cho cả client và server, tự inject/extract traceparent qua metadata, sinh span CLIENT/SERVER đặt tên theo method gRPC được gọi (semconv quy định {rpc.method}; instrumentation thực tế dùng đường dẫn đầy đủ dạng /package.Service/Method). Nhóm semconv RPC đang ở Release Candidate; rpc.system và rpc.grpc.status_code đã deprecated, thay bằng rpc.system.name và rpc.response.status_code (xem Phần 9). Với NestJS gRPC transport, instrumentation này đủ để nối context vào handler.
Kết nối sống lâu: WebSocket, SSE, long-poll, gRPC streaming
Nguyên tắc trung tâm, nói trước mọi chi tiết: không dùng một span cho cả vòng đời kết nối. Một span mở suốt bốn tiếng thì không bao giờ được export đúng lúc (span chỉ đi qua onEnd của processor khi nó kết thúc), vượt xa decision_wait của tail sampling nên trace bị quyết định khi còn thiếu gần hết nhánh, làm timeline vô nghĩa (một thanh dài che hết mọi thứ), và giữ span cùng toàn bộ attribute/event trong RAM cả buổi — đúng triệu chứng rò bộ nhớ mô tả ở §11.3.
| Loại | Mô hình span đúng |
|---|---|
| WebSocket | Một span SERVER ngắn cho HTTP upgrade; mỗi message là một trace riêng, nối về handshake bằng Link |
| SSE | Span của request khởi tạo end khi header đã gửi; mỗi event đẩy đi là một trace riêng |
| Long-poll | Span SERVER bình thường, nhưng tách thời gian chờ ra attribute riêng |
| gRPC streaming | Instrumentation cho một span cho cả stream; thêm span per-message thủ công nếu stream sống lâu |
WebSocket
import { SpanKind, type SpanContext, trace } from '@opentelemetry/api';
import type { WebSocket } from 'ws';
declare function handle(msg: { type: string }): Promise<void>;
const wsTracer = trace.getTracer('hasaki.ws');
type TracedSocket = WebSocket & { otelLink?: SpanContext };
// 1) Handshake: nếu upgrade đi qua một request HTTP bình thường (Socket.IO polling, hoặc
// route /ws có middleware) thì span SERVER đã có sẵn. Với sự kiện 'upgrade' thuần thì
// instrumentation-http KHÔNG tạo span — tự mở một span ngắn rồi end ngay tại đây.
// Việc còn lại chỉ là GIỮ LẠI span context của span handshake.
export function onConnection(socket: TracedSocket) {
socket.otelLink = trace.getActiveSpan()?.spanContext();
}
// 2) Mỗi message = MỘT trace riêng, root span, mang Link trỏ về handshake
export function onMessage(socket: TracedSocket, raw: string) {
const msg = JSON.parse(raw) as { type: string; orderId?: string };
return wsTracer.startActiveSpan(
`ws ${msg.type}`, // low cardinality: type, KHÔNG phải id
{
kind: SpanKind.SERVER,
root: true, // cắt khỏi trace của handshake
links: socket.otelLink ? [{ context: socket.otelLink }] : [],
attributes: {
'hasaki.ws.message_type': msg.type,
'hasaki.order.id': msg.orderId,
},
},
async (span) => {
try {
await handle(msg);
} finally {
span.end();
}
},
);
}
Nếu client là app của chính mình thì tốt hơn nữa: nhúng traceparent vào payload của từng message rồi propagation.extract ở server — y hệt pattern BullMQ ở trên, chỉ đổi carrier từ job.data._otel sang một field của message. Khi đó mỗi message nối đúng vào trace của hành động người dùng đã sinh ra nó, thay vì chỉ có một Link mờ nhạt về handshake.
SSE và long-poll
Với SSE, request khởi tạo trả header rồi giữ kết nối mở hàng giờ. Đừng để span SERVER sống cùng kết nối: kết thúc nó ngay sau khi header đã flush — đó mới là phần "xử lý" thật. Mỗi event đẩy xuống sau đó thuộc về trace của sự kiện nghiệp vụ sinh ra nó (đơn hàng đổi trạng thái), với một span PRODUCER ngắn cho việc đẩy event. Nếu framework không cho end sớm span SERVER thì ít nhất loại route SSE khỏi tracing bằng ignoreIncomingRequestHook rồi tự tạo span ngắn thay thế.
Long-poll thì span SERVER vẫn đúng mô hình (một request, một response), nhưng duration của nó không đo công việc: 29 trong 30 giây là ngồi chờ. Để p99 còn đọc được, tách hai con số ra:
import { trace } from '@opentelemetry/api';
declare const waitMs: number;
declare const workMs: number;
trace.getActiveSpan()?.setAttributes({
'hasaki.longpoll.wait_ms': waitMs, // thời gian chờ sự kiện
'hasaki.longpoll.work_ms': workMs, // thời gian xử lý thật
});
Và loại route long-poll khỏi dashboard latency chung, nếu không p99 của cả service bị kéo lên đúng bằng timeout đã cấu hình.
gRPC streaming
@opentelemetry/instrumentation-grpc tạo một span cho cả stream: mở khi stream bắt đầu, đóng khi stream kết thúc. Với unary call thì đúng; với server-streaming hoặc bidirectional stream chạy dài thì rơi lại đúng vấn đề ở đầu mục này. Stream ngắn vài giây thì chấp nhận được; stream sống lâu thì tự tạo span per-message bên trong handler bằng withSpan, coi span stream chỉ như cái khung, và bỏ nó khỏi mọi thống kê latency.
Ba hệ quả vận hành
decision_wait(Phần 4): mô hình "mỗi message một trace" giữ trace ngắn nên tail sampling quyết định trên trace đầy đủ. Ngược lại, một span kết nối dài luôn bị quyết định khi mới có vài span đầu tiên.- Thống kê latency: span handshake dài 8ms không nói gì về chất lượng của kết nối kéo dài ba tiếng sau đó. Chất lượng realtime phải đo bằng metrics riêng (số kết nối đang mở, độ trễ từ lúc sự kiện xảy ra tới lúc client nhận), không đọc từ duration của span.
BatchSpanProcessor: một burst message sinh hàng nghìn trace nhỏ trong vài giây.maxQueueSizemặc định 2048 sẽ tràn và span bị drop im lặng (Phần 5A, Phần 11). Hoặc nângmaxQueueSize/maxExportBatchSize, hoặc sample luồng realtime ở tỉ lệ thấp hơn phần còn lại.
Custom business attributes
Dùng namespace riêng của tổ chức, không mượn tiền tố của semconv:
- Dùng:
hasaki.order.id,hasaki.tenant.id— chữ thường, phân cấp bằng dấu chấm. - Không dùng:
http.*,db.*,messaging.*,url.*,server.*,otel.*— semconv sở hữu các tiền tố này; ghi đè sẽ phá phân tích của backend và xung đột khi semconv mở rộng.
| Attribute | Ví dụ giá trị | Vì sao hữu ích |
|---|---|---|
hasaki.order.id |
ORD-2026-887431 |
Nhảy từ ticket CSKH sang trace |
hasaki.order.item_count |
7 |
Tương quan giữa giỏ lớn và độ trễ |
hasaki.order.total_vnd |
1250000 |
Ưu tiên điều tra đơn giá trị cao |
hasaki.payment.method |
momo, cod, vnpay |
So sánh tỷ lệ lỗi theo cổng thanh toán |
hasaki.promotion.code |
SALE1010 |
Truy vết mã khuyến mãi gây chậm |
hasaki.tenant.id |
hasaki-vn |
Tách trace theo tenant |
hasaki.channel |
web, app_ios, app_android |
Phân tích theo kênh |
Không bao giờ ghi email, số điện thoại, số thẻ, CCCD, địa chỉ, token/JWT hay session cookie vào attribute. Trace store thường được nhiều team đọc và giữ lâu. Cần định danh khách hàng thì dùng ID nội bộ đã pseudonymous. Về semconv: enduser.id và enduser.pseudo.id vẫn ở mức Development kèm cảnh báo chứa PII; enduser.role (thay bằng user.roles) và enduser.scope đã Deprecated.
Attribute quá dài cũng là chi phí: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT mặc định 128, nhưng giới hạn độ dài giá trị mặc định là "không giới hạn" — dán nguyên request body vào attribute sẽ làm phình payload OTLP (xem Phần 10).
Log correlation
Mục tiêu: mỗi dòng log mang trace_id và span_id để nhảy hai chiều giữa log và trace.
Pino
// logger.ts
import { trace } from '@opentelemetry/api';
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
mixin() {
const span = trace.getActiveSpan();
if (!span) return {};
const { traceId, spanId, traceFlags } = span.spanContext();
// trace_flags dạng 2 hex: '01' = sampled
return {
trace_id: traceId,
span_id: spanId,
trace_flags: traceFlags.toString(16).padStart(2, '0'),
};
},
});
Winston
// logger.winston.ts
import { trace } from '@opentelemetry/api';
import winston from 'winston';
const otelFormat = winston.format((info) => {
const span = trace.getActiveSpan();
if (span) {
const { traceId, spanId, traceFlags } = span.spanContext();
info.trace_id = traceId;
info.span_id = spanId;
info.trace_flags = traceFlags.toString(16).padStart(2, '0');
}
return info;
});
export const logger = winston.createLogger({
format: winston.format.combine(otelFormat(), winston.format.json()),
transports: [new winston.transports.Console()],
});
Cách còn lại là để @opentelemetry/instrumentation-pino / @opentelemetry/instrumentation-winston tự inject — chúng vá logger và thêm các field này mà không phải sửa code. Bản viết tay có lợi thế là kiểm soát chính xác tên field, định dạng, và không phụ thuộc vào việc logger có được vá đúng thời điểm hay không.
Vì sao tên field là trace_id/span_id, và cách nhảy sang trace
Tên field phải là trace_id / span_id / trace_flags (snake_case) vì đó là quy ước của OTel log data model và là tên mà cả hai instrumentation trên sinh ra; các cấu hình mẫu của Grafana/Loki, Elastic và Jaeger đều mặc định theo tên này. Đặt traceId camelCase khiến mọi cấu hình sẵn có phải sửa tay.
| Backend | Cách nhảy từ log sang trace |
|---|---|
| Jaeger | Copy trace_id từ log, dán vào ô tìm kiếm của UI (port 16686) — Jaeger nhận trace ID trực tiếp |
| Grafana + Loki | Cấu hình derived field trên datasource Loki: regex bắt trace_id"?[:=]\s*"?([a-f0-9]{32}), trỏ tới datasource Jaeger; log line sẽ có nút mở trace |
| Elastic/OpenSearch | Index trace_id kiểu keyword, tạo link ra Jaeger theo mẫu /trace/{trace_id} |
Chiều ngược lại (trace → log) cần log store truy vấn được theo trace_id — lý do nên log JSON có cấu trúc thay vì text.
Testing span
// tests/order.trace.test.ts — Vitest
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { context, SpanStatusCode, trace } from '@opentelemetry/api';
import {
BasicTracerProvider,
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import { withSpan } from '../src/observability/with-span.js';
const exporter = new InMemorySpanExporter();
let provider: BasicTracerProvider;
beforeAll(() => {
// SimpleSpanProcessor export ngay khi span end -> assert được đồng bộ
provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] });
context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
// BasicTracerProvider#register() đã bị xóa ở 2.x -> set global thủ công
trace.setGlobalTracerProvider(provider);
});
afterEach(() => exporter.reset()); // reset giữa các test -> không phụ thuộc thứ tự
afterAll(async () => { await provider.shutdown(); });
describe('checkout tracing', () => {
it('tạo đúng quan hệ cha-con và attributes', async () => {
await withSpan('checkout', { attributes: { 'hasaki.order.id': 'ORD-1' } }, async () => {
await withSpan('reserve stock', async () => undefined);
});
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(2);
// Tìm theo tên, không dựa vào index
const parent = spans.find((s) => s.name === 'checkout')!;
const child = spans.find((s) => s.name === 'reserve stock')!;
expect(parent.attributes['hasaki.order.id']).toBe('ORD-1');
expect(child.parentSpanContext?.spanId).toBe(parent.spanContext().spanId);
expect(child.spanContext().traceId).toBe(parent.spanContext().traceId);
expect(parent.status.code).toBe(SpanStatusCode.UNSET);
});
it('set status ERROR và ghi exception khi lỗi', async () => {
await expect(
withSpan('checkout', async () => { throw new Error('out of stock'); }),
).rejects.toThrow('out of stock');
const [span] = exporter.getFinishedSpans();
expect(span.name).toBe('checkout');
expect(span.status.code).toBe(SpanStatusCode.ERROR);
expect(span.status.message).toBe('out of stock');
expect(span.events.map((e) => e.name)).toContain('exception');
expect(span.events[0].attributes?.['exception.message']).toBe('out of stock');
});
});
Ba điểm giữ cho test không giòn:
- Luôn
exporter.reset()trongafterEach; nếu không, test sau đếm cả span của test trước. - Tìm span theo tên, không theo index — thứ tự trong
getFinishedSpans()là thứ tự kết thúc, nên span con luôn xuất hiện trước span cha. - Dùng
SimpleSpanProcessor, khôngBatchSpanProcessor: batch chỉ export theo timer nên assert sẽ thấy mảng rỗng.
Lưu ý version: ở SDK 2.x trường parentSpanId trên ReadableSpan đã được thay bằng parentSpanContext (span.parentSpanId → span.parentSpanContext?.spanId), và BasicTracerProvider#register() đã bị xóa. Với Jest, cấu trúc y hệt, chỉ đổi import sang @jest/globals và đưa phần setup vào setupFilesAfterEnv.
Nên tạo span và không nên tạo span
| Nên tạo span | Không nên tạo span |
|---|---|
| Mọi biên I/O: HTTP call, query DB, publish/consume message, đọc-ghi object storage | Vòng lặp chặt (hàng nghìn vòng) — chi phí span vượt xa chi phí công việc |
| Ranh giới service, ranh giới process | Getter, setter, hàm map/filter thuần |
| Tác vụ chậm có ý nghĩa nghiệp vụ: tính giá giỏ hàng, chấm điểm gian lận, render PDF hóa đơn | Hàm thuần tính toán chạy dưới mili-giây |
| Handler của job/cron | Mỗi bản ghi trong batch — thay bằng một span cho cả batch + attribute đếm (hasaki.batch.size, hasaki.batch.failed_count) |
| Retry (mỗi lần thử một span, hoặc span cha kèm event) | Mỗi lần đọc cache in-memory |
| Đoạn code nghi là nút thắt, trong thời gian điều tra | Wrapper chỉ gọi lại một hàm đã có span |
Ngưỡng thực dụng: một span dự kiến ngắn hơn ~1ms và không phải I/O thì gần như chắc chắn là nhiễu. Trace 500 span cho một request không dễ đọc hơn trace 20 span — nó khó đọc hơn, tốn tiền hơn, và làm chậm chính ứng dụng. Giới hạn mặc định 128 span event và 128 link mỗi span là dấu hiệu rõ về quy mô mà SDK kỳ vọng (xem Phần 10).
Phụ lục — Frontend/browser tracing (ngoài phạm vi Node.js)
Mục này không phải Node.js. Nó nằm ở đây vì ba mảnh cấu hình của tracing phía browser phải khớp nhau mới chạy — SDK trong bundle, CORS của API, receiver của Collector — mà chúng lại thuộc ba phần khác nhau (Phần 3, Phần 6, Phần 7). Gom về một chỗ để đọc một lượt.
Khác biệt cơ bản so với Node
| Node.js | Browser | |
|---|---|---|
| Context propagation | AsyncLocalStorage, tự động qua mọi await |
Không có AsyncLocalStorage; hoặc ZoneContextManager (kéo theo zone.js), hoặc chấp nhận context phẳng và tự truyền tay |
| Bí mật | env var trong container | Mọi thứ trong bundle là công khai — không có chỗ giấu API key |
| Sampling | quyết định ở SDK hoặc ở Collector, đều do mình kiểm soát | phải quyết định ở client: span không sinh ra thì không tốn cả băng thông của người dùng lẫn tiền ingest |
| Khối lượng | tỉ lệ với số request | tỉ lệ với số pageview — thường lớn hơn một bậc |
| Mạng | nội bộ, tin cậy | qua Internet: có adblocker chặn, có CORS, có người cố tình bơm span rác |
Khởi tạo
// browser-tracing.ts
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const provider = new WebTracerProvider({
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'hasaki-web' }),
// SDK 2.x: span processor truyền qua constructor, KHÔNG có addSpanProcessor
spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter({ url: 'https://otel.hasaki.vn/v1/traces' })),
],
});
provider.register();
registerInstrumentations({
instrumentations: [
new DocumentLoadInstrumentation(),
new FetchInstrumentation({
// Chỉ inject traceparent vào origin do mình sở hữu
propagateTraceHeaderCorsUrls: [/^https:\/\/api\.hasaki\.vn/],
}),
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [/^https:\/\/api\.hasaki\.vn/],
}),
],
});
CORS phải khớp ở cả ba nơi
Thiếu một trong ba là trace đứt tại biên browser mà không có lỗi nào dễ đọc:
- Client —
propagateTraceHeaderCorsUrlstrong khối trên quyết định origin nào được nhận headertraceparent. Mặc định SDK không inject header sang cross-origin, để tránh gửi trace context của mình sang bên thứ ba. - Server API —
traceparentlà custom header nên trình duyệt gửi preflightOPTIONS; API phải trảAccess-Control-Allow-Headers: traceparent, tracestate(thêmbaggagenếu dùng). Thiếu dòng này thì request bị chặn hoặc header bị strip (Phần 3). - Collector nhận OTLP từ browser — receiver OTLP/HTTP phải khai
cors(Phần 7):
receivers:
otlp/browser:
protocols:
http:
endpoint: 0.0.0.0:4319 # cổng riêng, tách khỏi 4318 nội bộ
cors:
allowed_origins: [https://www.hasaki.vn] # liệt kê đúng origin, KHÔNG dùng "*"
allowed_headers: [traceparent, tracestate, baggage, content-type]
max_age: 7200 # giây, cache preflight OPTIONS
Endpoint nhận trace từ browser
Endpoint này bắt buộc public, và mọi thứ nhúng trong JS đều đọc được — nên đừng dùng chung với pipeline nội bộ:
- Collector riêng, cổng riêng, chỉ HTTP (browser không nói gRPC trực tiếp được).
- Không nhúng API key hay token vào bundle. Token trong bundle là token đã lộ. Chống lạm dụng bằng rate limit ở ingress, giới hạn body size ở receiver, và CORS liệt kê đúng origin.
- Pipeline riêng có
transformghi đèservice.nametheo cổng đã nhận, thay vì tin giá trị client khai — nếu không, bất kỳ ai cũng bơm được span mangservice.namecủa team khác (Phần 7). - Không tin
traceparentdo client gửi ở rìa public: sampled flag01từ nguồn không tin cậy là một cách ép hệ thống ghi 100% trace (Phần 10).
Khối lượng và chi phí
Một pageload sinh hàng chục span: documentLoad, mỗi resource, mỗi fetch/XHR. Nhân với số pageview thật, browser thường là nguồn span lớn nhất và đắt nhất của cả hệ thống, vượt xa backend. Ba việc phải làm trước khi bật cho 100% người dùng:
- Sampling ở client với tỉ lệ thấp (
TraceIdRatioBasedSamplertruyền vàoWebTracerProvider) — Phần 4. - Giới hạn span resource của
DocumentLoadInstrumentationnếu trang tải hàng trăm asset. - Đưa con số ước lượng vào công thức chi phí ở Phần 10 trước khi bật, không phải sau khi nhận hoá đơn.
Tra cứu sâu: Instrumentation cho JS, opentelemetry-js-contrib, semconv messaging spans.
Phần 6 — OTLP: giao thức và biến môi trường
OTLP là gì và nằm ở đâu trong hệ thống
OTLP (OpenTelemetry Protocol) là wire protocol chuẩn của OpenTelemetry: định nghĩa cách một process đóng gói trace, metric, log rồi đẩy sang process khác. Trước đây mỗi backend có protocol riêng — Jaeger dùng Thrift/gRPC jaeger.api_v2, Zipkin dùng JSON /api/v2/spans — nên đổi backend là đổi exporter, đổi thư viện, build lại image. OTLP xóa ràng buộc đó: SDK nói OTLP, Collector nhận và gửi lại OTLP, backend nhận OTLP. Jaeger v2 nhận OTLP native trên 4317/4318, nên @opentelemetry/exporter-jaeger và Jaeger exporter trong Collector đều không còn cần thiết (Jaeger exporter đã bị gỡ khỏi opentelemetry-collector-contrib).
Hệ quả thực tế: ba đoạn dưới đây ghép tùy ý mà không đổi code ứng dụng, chỉ đổi endpoint.
flowchart LR
A["Node.js SDK<br/>OTLP exporter"] -->|OTLP| B["OTel Collector<br/>(tùy chọn)"]
A -->|OTLP| C["Jaeger v2<br/>4317 / 4318"]
B -->|OTLP| C
B -->|OTLP| D["Backend khác<br/>(SaaS, ClickHouse...)"]
OTLP không đánh số phiên bản giao thức — spec ghi nguyên văn "OTLP does not use explicit protocol version numbering". Con số v1 trong đường dẫn proto (opentelemetry/proto/trace/v1) là package version của file proto, còn các release vX.Y.Z là version của repo opentelemetry-proto, không phải của protocol. Đừng viết "OTLP 1.x" trong tài liệu vận hành. Trạng thái: encoding cho trace/metric/log là Stable (cả binary protobuf lẫn JSON); profiles còn ở mức Development.
Ba transport
| Transport | Port mặc định | Package JS | Ưu điểm | Nhược điểm | Khi nào chọn |
|---|---|---|---|---|---|
grpc |
4317 | @opentelemetry/exporter-trace-otlp-grpc |
Throughput cao nhất, multiplex trên một kết nối HTTP/2 | Cần HTTP/2 end-to-end; nhiều ingress/proxy/WAF làm hỏng hoặc downgrade; khó debug bằng curl | Service tới Collector trong cùng cluster/VPC, volume span lớn |
http/protobuf |
4318, path /v1/traces |
@opentelemetry/exporter-trace-otlp-proto |
Payload nhỏ như gRPC nhưng đi qua HTTP/1.1, qua proxy/ingress/L7 LB dễ | Mỗi lần export là một request riêng, overhead kết nối cao hơn gRPC | Mặc định nên dùng; bắt buộc khi phải qua ingress, API gateway hoặc endpoint SaaS |
http/json |
4318, path /v1/traces |
@opentelemetry/exporter-trace-otlp-http |
Đọc được bằng mắt, test bằng curl, không cần schema | Payload lớn hơn nhiều, tốn CPU serialize; spec chỉ coi là MAY nên không phải backend nào cũng nhận | Debug thủ công, môi trường dev, browser |
Cả ba package exporter đều nằm ở dòng experimental 0.x của OTel JS (0.222.0 tại thời điểm biên soạn), README ghi rõ "This is an experimental package under active development. New releases may include breaking changes." — luôn pin version chính xác trong package.json.
Khuyến nghị: mặc định chọn http/protobuf, cũng là default của spec cho OTEL_EXPORTER_OTLP_PROTOCOL. Chỉ chuyển sang grpc khi đã đo được exporter là nút thắt và chắc chắn HTTP/2 thông suốt (không có proxy TLS-terminate rồi nói HTTP/1.1 ở chặng sau). http/json không dùng cho production trên đường nóng.
Một khác biệt hay gây nhầm: exporter gRPC nhận URL chỉ có host:port, không có path /v1/traces — nếu đưa path vào, OTel JS ghi diag warn URL path should not be set when using grpc, the path part of the URL will be ignored. rồi bỏ phần path. Exporter HTTP thì ngược lại, endpoint phải kết thúc bằng /v1/traces.
Header Content-Type phân biệt hai encoding HTTP: application/x-protobuf cho http/protobuf, application/json cho http/json. Gửi sai header này thường nhận 415 Unsupported Media Type.
Cấu trúc payload
Một lần export trace là một ExportTraceServiceRequest, lồng ba tầng:
ExportTraceServiceRequest
└── resourceSpans[] // nhóm theo Resource (một service/instance)
├── resource // attributes: service.name, service.version, host.name...
└── scopeSpans[] // nhóm theo InstrumentationScope
├── scope // name (+ version) của thư viện instrumentation
└── spans[] // các span thực sự
Lồng ba tầng là để khử trùng lặp: trong một batch 512 span của cùng một pod, resource chỉ xuất hiện một lần thay vì 512 lần, và mỗi scope (@opentelemetry/instrumentation-http, instrumentation-pg...) cũng chỉ xuất hiện một lần cho nhóm span của nó. Đây là lý do batch càng lớn thì chi phí byte trên mỗi span càng giảm — liên quan trực tiếp tới chi phí egress, xem Phần 10.
Ví dụ đầy đủ một request http/json chứa đúng một span:
{
"resourceSpans": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } },
{ "key": "service.namespace", "value": { "stringValue": "ecommerce" } },
{ "key": "deployment.environment.name", "value": { "stringValue": "production" } }
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": { "name": "@opentelemetry/instrumentation-http" },
"spans": [
{
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"parentSpanId": "051581bf3cb55c13",
"flags": 769,
"name": "POST /api/orders",
"kind": 2,
"startTimeUnixNano": "1757300000000000000",
"endTimeUnixNano": "1757300000148000000",
"attributes": [
{ "key": "http.request.method", "value": { "stringValue": "POST" } },
{ "key": "http.route", "value": { "stringValue": "/api/orders" } },
{ "key": "http.response.status_code", "value": { "intValue": "201" } },
{ "key": "url.path", "value": { "stringValue": "/api/orders" } },
{ "key": "server.address", "value": { "stringValue": "checkout-api" } }
],
"events": [
{
"timeUnixNano": "1757300000100000000",
"name": "cart.locked",
"attributes": [
{ "key": "cart.id", "value": { "stringValue": "c-91823" } }
]
}
],
"links": []
}
]
}
]
}
]
}
Ghi chú đọc payload:
kind:1INTERNAL,2SERVER,3CLIENT,4PRODUCER,5CONSUMER (0= UNSPECIFIED).status.code:0UNSET,1OK,2ERROR.- Span ở trên cố tình không có trường
status: field vắng mặt nghĩa làUNSET(0) — đúng cho một span SERVER trả2xx.OK(1) chỉ dành cho application code muốn khẳng định tường minh; instrumentation HTTP không bao giờ tự đặtOK. Copy payload này làm mẫu thì đừng thêm"status": { "code": 1 }. Xem Phần 1 và Phần 9. flagslà bit-field: 8 bit thấp là W3C trace-flags (0x01= sampled),0x100báo "đã biết parent có remote hay không",0x200báo parent là remote. Giá trị769=0x301nghĩa là sampled và parent đến từ traceparent của service khác. Xem Phần 3 về trace-flags.- Mọi mốc thời gian là nanosecond tính từ Unix epoch, gửi dưới dạng chuỗi vì vượt
Number.MAX_SAFE_INTEGER.intValuecũng là chuỗi (proto3 JSON mapping cho int64). - Trong OTLP/JSON,
traceIdvàspanIdlà chuỗi hex (case-insensitive), không phải base64 — đây là điểm OTLP cố tình lệch khỏi proto3 JSON mapping chuẩn. Tronghttp/protobufvàgrpcchúng là byte thô (16 byte và 8 byte). - Tên attribute trong ví dụ là bộ semantic conventions HTTP Stable; mapping từ bộ cũ (
http.method,http.status_code...) xem Phần 9.
Tham chiếu: https://opentelemetry.io/docs/specs/otlp/ và https://github.com/open-telemetry/opentelemetry-proto
Response, partial success và retry
Với OTLP/HTTP, server trả ExportTraceServiceResponse. Bốn trường hợp:
| Trường hợp | HTTP | Body | Client phải làm gì |
|---|---|---|---|
| Thành công hoàn toàn | 200 | partial_success không được set |
Không làm gì |
| Partial success | 200 | partial_success có rejected_spans > 0 và error_message |
MUST NOT retry. Ghi log, đếm số span mất |
| Cảnh báo (nhận hết) | 200 | rejected_spans = 0 nhưng error_message khác rỗng |
Không retry; đọc error_message (thường là cảnh báo deprecation từ backend) |
| Lỗi | 4xx/5xx | message google.rpc.Status, encode cùng định dạng với request |
Retry theo bảng dưới, hoặc drop |
Điểm dễ sai nhất: partial success trả về HTTP 200. Nếu chỉ theo dõi status code, bạn sẽ thấy "export thành công" trong khi backend đang từ chối một phần span — thường do vượt giới hạn attribute, timestamp quá cũ, hoặc rate limit theo tenant. Phải log partial_success.error_message.
Lưu ý tên field: partial_success / rejected_spans / error_message là tên trong file .proto; khi đọc body JSON thật thì chúng xuất hiện dưới dạng lowerCamelCase partialSuccess / rejectedSpans / errorMessage theo proto3 JSON mapping.
| Mã | Retry? | Ghi chú |
|---|---|---|
| 400 | Không | Payload hỏng, không decode được. Retry chỉ tốn băng thông |
| 401 / 403 | Không | Sai token — sửa cấu hình |
| 404 | Không | Gần như luôn là sai path /v1/traces (xem bẫy endpoint bên dưới) |
| 429 | Có | Tôn trọng header Retry-After nếu có |
| 500 | Không | Không nằm trong danh sách retryable của spec |
| 502 / 503 / 504 | Có | 503 cũng có thể kèm Retry-After |
| Kết nối đứt, không có response | Có | Retry cùng request với exponential backoff + jitter |
Retry-After (RFC 9110, trước đây RFC 7231) có thể là số giây hoặc HTTP-date. Với OTLP/gRPC, spec liệt kê các status code được retry là CANCELLED, DEADLINE_EXCEEDED, ABORTED, OUT_OF_RANGE, UNAVAILABLE, DATA_LOSS; riêng RESOURCE_EXHAUSTED chỉ retry khi server báo khả năng phục hồi bằng RetryInfo trong status details. UNKNOWN, INVALID_ARGUMENT, PERMISSION_DENIED, UNIMPLEMENTED, INTERNAL, UNAUTHENTICATED là không retry — hành vi tương tự 400/401/500 bên HTTP.
Retry MUST dùng exponential backoff có jitter và MUST có giới hạn. Retry vô hạn biến sự cố Collector thành sự cố ứng dụng: queue BatchSpanProcessor đầy, span mới bị drop, CPU và socket bị exporter chiếm, backpressure lan ngược vào request path. Exporter OTLP của OTel JS đã cài sẵn chính sách này và hiện không cấu hình được (hằng số trong otlp-exporter-base, file retrying-transport): tối đa 5 lần retry sau lần gửi đầu — tổng cộng tối đa 6 request, backoff khởi điểm 1s, trần 5s, hệ số nhân 1.5, jitter ±20%, ưu tiên Retry-After nếu response có header đó, và toàn bộ chuỗi retry phải nằm gọn trong timeout của export (khi thời điểm retry kế tiếp vượt quá deadline còn lại, exporter dừng và ghi diag info). Lưu ý README của package ghi DEFAULT_EXPORT_MAX_ATTEMPTS là "maximum number of attempts, including the original request" — mã thực tế ở 0.222.0 khởi tạo attempts = MAX_ATTEMPTS sau lần gửi đầu tiên, nên con số hiệu dụng là 5 retry chứ không phải 5 lần thử tổng cộng. Cần chính sách mạnh hơn (persistent queue trên đĩa, retry hàng giờ) thì đó là việc của Collector — xem Phần 7.
Compression, TLS, mTLS và authentication
# Bật gzip: gần như luôn nên bật cho traffic ra khỏi node/cluster
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip
# TLS: chỉ cần dùng scheme https trong endpoint
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.internal.example.vn:4318
# mTLS
export OTEL_EXPORTER_OTLP_CERTIFICATE=/etc/otel/ca.crt
export OTEL_EXPORTER_OTLP_CLIENT_KEY=/etc/otel/client.key
export OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/etc/otel/client.crt
# Auth: định dạng W3C Baggage, các cặp cách nhau bằng dấu phẩy
# Dấu cách trong "Bearer <token>" nên percent-encode thành %20
export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer%20${OTEL_TOKEN},x-tenant=shop-prod"
OTEL_EXPORTER_OTLP_HEADERS dùng cú pháp key1=value1,key2=value2 (định dạng W3C Baggage); spec ghi rõ không hỗ trợ metadata phân cách bằng dấu chấm phẩy, và mọi giá trị đều được coi là chuỗi. Giá trị chứa ký tự đặc biệt (kể cả dấu cách trong Bearer <token>) nên percent-encode; OTel JS chạy decodeURIComponent trên phần value khi parse. Với grpc, biến này được nạp thành gRPC metadata — nhưng option headers truyền bằng code thì exporter gRPC bỏ qua (issue opentelemetry-js#5520 còn mở); phải dùng metadata (và credentials) của @grpc/grpc-js:
import { readFileSync } from 'node:fs';
import { credentials, Metadata } from '@grpc/grpc-js';
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
const metadata = new Metadata();
metadata.set('x-tenant', 'shop-prod');
// mTLS: CA, client key, client cert
const exporter = new OTLPTraceExporter({
url: 'https://otel.internal.example.vn:4317', // gRPC: KHÔNG có /v1/traces
credentials: credentials.createSsl(
readFileSync('/etc/otel/ca.crt'),
readFileSync('/etc/otel/client.key'),
readFileSync('/etc/otel/client.crt'),
),
metadata,
compression: CompressionAlgorithm.GZIP,
timeoutMillis: 10_000,
});
CompressionAlgorithm là enum (NONE / GZIP) export từ @opentelemetry/otlp-exporter-base, nên ở TypeScript đừng truyền chuỗi 'gzip' trần. Về credentials mặc định: chỉ scheme http:// mới cho insecure; https:// và cả URL không ghi scheme (collector:4317) đều được chuẩn hóa thành TLS. Truyền credentials tường minh sẽ ghi đè lựa chọn đó.
Cảnh báo về secret:
- Token trong biến môi trường lộ ra qua
docker inspect,kubectl describe pod, crash dump và mọi log dumpprocess.env. Trong Kubernetes hãy đưa qua Secret +envFrom; tốt hơn nữa là cho service gửi OTLP không xác thực tới Collector nội bộ, và chỉ Collector giữ token đi ra ngoài. - Không bao giờ đặt
OTEL_EXPORTER_OTLP_HEADERSchứa API key vào bundle frontend. Mọi thứ build vào browser bundle đều là public. Browser phải gửi OTLP tới một endpoint proxy của chính bạn (Collector có bật CORS), không gửi thẳng lên SaaS kèm key. OTEL_EXPORTER_OTLP_INSECURE=truechỉ dành cho dev/localhost.
Bảng biến môi trường OTLP
Mọi biến OTEL_EXPORTER_OTLP_* đều có bản per-signal OTEL_EXPORTER_OTLP_TRACES_*, và bản per-signal luôn thắng bản chung.
| Biến | Ý nghĩa | Default |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
Base URL dùng chung cho mọi signal. Với HTTP, SDK tự nối v1/traces |
http://localhost:4318 (HTTP), http://localhost:4317 (gRPC) |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
Endpoint riêng cho traces, dùng nguyên văn (as-is); nếu set thì thắng biến chung | Không có default riêng — không set thì rơi về biến chung, giá trị hiệu dụng là http://localhost:4318/v1/traces (HTTP) hoặc http://localhost:4317 (gRPC) |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc | http/protobuf | http/json |
http/protobuf |
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL |
Như trên, riêng cho traces | kế thừa biến chung |
OTEL_EXPORTER_OTLP_HEADERS |
Header đính vào mỗi request, cú pháp k1=v1,k2=v2 |
không có |
OTEL_EXPORTER_OTLP_TRACES_HEADERS |
Như trên, riêng cho traces | không có |
OTEL_EXPORTER_OTLP_TIMEOUT |
Timeout mỗi lần export, đơn vị ms | 10000 |
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT |
Như trên, riêng cho traces | 10000 |
OTEL_EXPORTER_OTLP_COMPRESSION |
gzip | none |
không set (không nén) |
OTEL_EXPORTER_OTLP_CERTIFICATE |
Đường dẫn file CA certificate (PEM) để verify server | không có |
OTEL_EXPORTER_OTLP_CLIENT_KEY |
Đường dẫn private key của client (mTLS) | không có |
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE |
Đường dẫn client certificate (mTLS) | không có |
OTEL_EXPORTER_OTLP_INSECURE |
Bỏ TLS. Chỉ áp dụng cho gRPC khi endpoint không có scheme http/https |
false |
Spec khuyến nghị mặc định 64 MiB (67108864 bytes) cho kích thước request ở cả client lẫn server, và 4 MiB cho response. Không có env var chuẩn cho các giới hạn này — chúng là tham số của thư viện gRPC/HTTP phía client hoặc của receiver phía Collector.
Bẫy quan trọng: ENDPOINT nối path, TRACES_ENDPOINT thì không
Quy tắc của spec, chỉ áp dụng cho OTLP/HTTP:
OTEL_EXPORTER_OTLP_ENDPOINTlà base URL, SDK nối thêmv1/traces(vàv1/metrics,v1/logs).OTEL_EXPORTER_OTLP_<signal>_ENDPOINTđược dùng "as-is without any modification". Không có path thì dùng đúng root/.
| Cấu hình | URL thực tế | Kết quả |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 |
http://collector:4318/v1/traces |
Đúng |
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/v1/traces |
http://collector:4318/v1/traces/v1/traces |
Sai — 404 |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://collector:4318 |
http://collector:4318/ |
Sai — 404, Jaeger không có dữ liệu |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://collector:4318/v1/traces |
http://collector:4318/v1/traces |
Đúng |
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/mycollector/ |
http://collector:4318/mycollector/v1/traces |
Đúng (path sẵn có được giữ làm base) |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://collector:4317/v1/traces với protocol grpc |
http://collector:4317 |
Path bị bỏ qua, OTel JS ghi cảnh báo diag "the path part of the URL will be ignored". Viết đúng là http://collector:4317 |
Triệu chứng khi sai: ứng dụng chạy bình thường, không ném exception, nhưng Jaeger trống trơn. Bật log nội bộ của SDK (xem Phần 11) sẽ thấy exporter báo 404. Quy tắc nối path không áp dụng cho gRPC — gRPC chỉ dùng host:port.
Các biến môi trường chung liên quan tracing
| Biến | Ý nghĩa | Default |
|---|---|---|
OTEL_SERVICE_NAME |
Đặt service.name; thắng giá trị service.name trong OTEL_RESOURCE_ATTRIBUTES |
không có; thiếu thì resource rơi về unknown_service:... — luôn set tay |
OTEL_RESOURCE_ATTRIBUTES |
Resource attributes, cú pháp k1=v1,k2=v2 |
theo resource semconv |
OTEL_TRACES_EXPORTER |
otlp | zipkin | console | none; logging deprecated, otlp/stdout còn ở mức development. jaeger không còn là giá trị hợp lệ |
otlp |
OTEL_PROPAGATORS |
tracecontext, baggage, b3, b3multi, xray, none (jaeger và ottrace deprecated). Riêng JS: bộ giải env dựng sẵn của @opentelemetry/sdk-node không hiểu xray/ottrace — cần package contrib @opentelemetry/auto-configuration-propagators. Xem Phần 3 |
tracecontext,baggage |
OTEL_TRACES_SAMPLER |
always_on, always_off, traceidratio, parentbased_*, jaeger_remote... Xem Phần 4 |
parentbased_always_on |
OTEL_TRACES_SAMPLER_ARG |
Tham số cho sampler; với traceidratio là số trong [0..1] |
rỗng (ratio ngầm định 1.0) |
OTEL_SDK_DISABLED |
true thì SDK chạy ở dạng no-op: không ghi, không export span |
false |
OTEL_LOG_LEVEL |
Mức log nội bộ của SDK | info |
OTEL_BSP_SCHEDULE_DELAY |
Khoảng cách giữa hai lần export của BatchSpanProcessor (ms) | 5000 |
OTEL_BSP_EXPORT_TIMEOUT |
Timeout một lần export của BSP (ms) | 30000 |
OTEL_BSP_MAX_QUEUE_SIZE |
Kích thước queue; đầy thì span bị drop | 2048 |
OTEL_BSP_MAX_EXPORT_BATCH_SIZE |
Số span mỗi batch; phải ≤ max queue size | 512 |
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT |
Số attribute tối đa mỗi span | 128 |
OTEL_SPAN_EVENT_COUNT_LIMIT |
Số event tối đa mỗi span | 128 |
OTEL_SPAN_LINK_COUNT_LIMIT |
Số link tối đa mỗi span | 128 |
OTEL_ATTRIBUTE_COUNT_LIMIT |
Giới hạn số attribute mặc định (span, event, link) | 128 |
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT |
Độ dài tối đa của giá trị attribute dạng chuỗi | không giới hạn |
OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT |
Như trên nhưng chỉ cho attribute của span; thắng biến ở trên | không giới hạn |
OTEL_SEMCONV_STABILITY_OPT_IN |
Chọn bộ semantic conventions: http, http/dup, database, database/dup, messaging, messaging/dup, rpc, rpc/dup. Xem Phần 9 |
rỗng (tiếp tục phát bộ cũ) |
Danh sách và default lấy từ https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/. Mức độ hỗ trợ từng biến khác nhau giữa các package JS — nếu một biến không có tác dụng, kiểm tra CHANGELOG/release notes của package tương ứng thay vì giả định.
Kiểm thử bằng tay
Gửi một span bằng curl
Chạy được ngay với Jaeger v2 hoặc Collector đang mở port 4318. Dùng thời gian hiện tại để span không rơi ra ngoài khung thời gian mặc định của UI:
NOW=$(date +%s)
curl -i -X POST http://localhost:4318/v1/traces \
-H 'Content-Type: application/json' \
-d '{
"resourceSpans": [{
"resource": { "attributes": [
{ "key": "service.name", "value": { "stringValue": "curl-smoketest" } }
]},
"scopeSpans": [{
"scope": { "name": "manual" },
"spans": [{
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"name": "POST /api/orders",
"kind": 2,
"startTimeUnixNano": "'"${NOW}"'000000000",
"endTimeUnixNano": "'"${NOW}"'150000000"
}]
}]
}]
}'
Kết quả mong đợi: HTTP/1.1 200 OK với body {} hoặc {"partialSuccess":{}}. Nhận 404 là sai path; 415 là thiếu Content-Type; 400 kèm thông báo decode là JSON sai kiểu — thường do để startTimeUnixNano thành số thay vì chuỗi. Gửi lại lần hai với cùng traceId sẽ thêm span vào cùng trace, tiện để kiểm tra việc gộp trace ở backend.
Sinh trace giả bằng telemetrygen
telemetrygen là công cụ trong opentelemetry-collector-contrib, dùng để bơm tải OTLP mà không cần ứng dụng thật:
go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest
# OTLP/gRPC vào 4317
telemetrygen traces --otlp-endpoint localhost:4317 --otlp-insecure \
--service checkout-api --traces 20
# OTLP/HTTP vào 4318
telemetrygen traces --otlp-endpoint localhost:4318 --otlp-insecure --otlp-http \
--service checkout-api --duration 30s --rate 50
Mặc định --otlp-endpoint là localhost:4317, nên khi dùng --otlp-http phải ghi rõ cổng 4318. Tên flag có thể đổi giữa các release; đối chiếu bằng telemetrygen traces --help.
Xác nhận Collector đã nhận
Cách nhanh nhất là thêm debug exporter với verbosity: detailed vào pipeline traces — mỗi span nhận được sẽ in ra stdout của Collector, đọc bằng docker logs hoặc kubectl logs. Cấu hình pipeline chi tiết ở Phần 7. Nếu log Collector im lặng, vấn đề nằm ở phía client (endpoint, protocol, firewall); nếu log có span nhưng Jaeger trống thì vấn đề nằm ở chặng exporter của Collector.
Ngoài ra, internal telemetry của Collector expose Prometheus trên port 8888, trong đó otelcol_receiver_accepted_spans và otelcol_receiver_refused_spans cho biết ngay số span vào và số bị từ chối:
curl -s http://localhost:8888/metrics | grep -E 'otelcol_receiver_(accepted|refused)_spans'
Phần 7 — OpenTelemetry Collector
Collector là một binary Go độc lập: nhận telemetry qua nhiều giao thức, chạy qua một chuỗi xử lý, rồi đẩy ra một hay nhiều backend. Nó không bắt buộc — SDK gửi thẳng OTLP lên Jaeger vẫn chạy — nhưng gần như mọi hệ thống production đều thêm nó vào.
7.1 Vì sao cần Collector thay vì export trực tiếp
| Vấn đề khi app export thẳng lên backend | Collector giải quyết thế nào |
|---|---|
| Đổi backend (Jaeger → SaaS, thêm backend thứ hai) phải sửa code, build lại image, deploy lại mọi service | Endpoint của app cố định trỏ vào Collector; thay đổi nằm ở config Collector |
| Mỗi service tự buffer/retry, logic khác nhau, khó kiểm chứng | Buffer + retry tập trung một chỗ, có sending_queue bền vững trên đĩa |
| Batch nhỏ vì mỗi pod chỉ có lưu lượng của riêng nó | Collector gom span của nhiều pod → batch lớn hơn, ít request hơn |
| PII lọt vào attribute ở hàng chục repo khác nhau | Một redaction/transform chặn được toàn bộ, kể cả span do thư viện bên thứ ba sinh ra |
| App không biết mình đang chạy pod nào, node nào | k8sattributes + resourcedetection gắn metadata hạ tầng sau khi nhận |
| Muốn gửi cùng một trace tới Jaeger và một hệ thống khác | Fan-out: một pipeline nhiều exporter |
| Tail sampling cần thấy toàn bộ trace | Chỉ làm được ở tầng tập trung, không làm được trong SDK (xem Phần 4) |
| Hàng trăm pod mở kết nối ra Internet tới backend SaaS | Chỉ Collector mở kết nối ra ngoài; egress/firewall gọn |
| API key của backend nằm trong env của mọi service | Credential chỉ đặt ở Secret của Collector |
Khi KHÔNG cần Collector: POC hoặc dev local với Jaeger; job ngắn hạn / serverless lưu lượng nhỏ không có chỗ đặt sidecar; một monolith, một backend, không yêu cầu xoá PII và không cần tail sampling. Khi đó Collector chỉ là một hop phải vận hành thêm — cứ export thẳng OTLP, tách ra khi gặp nhu cầu đầu tiên trong bảng trên.
7.2 Distributions và OpenTelemetry Collector Builder
Dự án phát hành 5 distribution chính thức, mỗi bản là một tập component đã biên dịch sẵn khác nhau:
| Distribution | Binary | Dùng khi |
|---|---|---|
| OpenTelemetry Collector (core) | otelcol |
Chỉ cần OTLP in/out, batch, memory_limiter, filter, attributes |
| OpenTelemetry Collector Contrib | otelcol-contrib |
Cần tail_sampling, k8sattributes, transform, spanmetrics, loadbalancing — phần lớn hệ thống thật rơi vào đây |
| OpenTelemetry Collector for Kubernetes | otelcol-k8s |
Bản rút gọn cho K8s; có k8sattributes, tail_sampling, loadbalancing, file_storage mà không kéo theo toàn bộ contrib |
| OpenTelemetry Collector OTLP | otelcol-otlp |
Chỉ nhận và chuyển tiếp OTLP, image tối thiểu |
| OpenTelemetry Collector eBPF Profiler | otelcol-ebpf-profiler |
Profiling qua eBPF |
Version hiện hành (chốt 2026-09-02): core phát hành dưới tên v1.66.0/v0.160.0 (module stable v1.66.0 + module beta v0.160.0), contrib v0.160.0, image quick-start otel/opentelemetry-collector:0.160.0. Có nhiều distro bên thứ ba (AWS, Datadog, Elastic, Splunk…) nhưng docs chính thức ghi rõ OpenTelemetry không validate hay endorse chúng.
otelcol-contrib chứa hàng trăm component bạn không dùng: image lớn, và mỗi component là một phần bề mặt tấn công cùng một nguồn CVE tiềm tàng. OpenTelemetry Collector Builder (ocb) build một bản chỉ gồm những gì bạn khai báo — docs ở /docs/collector/extend/ocb/ (URL cũ /docs/collector/custom-collector/ đã 404).
# Cách 1 — go install. Binary sinh ra tên là `builder` trong $GOPATH/bin, KHÔNG phải ./ocb
go install go.opentelemetry.io/collector/cmd/builder@v0.160.0
builder --config=builder-config.yaml
# Cách 2 — tải binary `ocb` từ GitHub releases (tag dạng cmd/builder/<version>) rồi gọi ./ocb
curl -L -o ocb https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/cmd%2Fbuilder%2Fv0.160.0/ocb_0.160.0_linux_amd64
chmod +x ocb && ./ocb --config builder-config.yaml
Đừng trộn hai cách: go install không tạo file ocb trong thư mục hiện hành nên ./ocb sẽ báo no such file or directory. Version builder phải khớp version module khai trong manifest, lệch thì ocb cảnh báo và bản build có thể không compile.
# builder-config.yaml — manifest tối giản cho e-commerce Node.js + Jaeger
dist:
name: otelcol-shop # mặc định là otelcol-custom
output_path: ./dist
version: 1.0.0 # version của BẢN BUILD, không phải của Collector
receivers:
- gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.160.0
processors:
- gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.160.0
- gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.160.0
- gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor v0.160.0
- gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor v0.160.0
exporters:
- gomod: go.opentelemetry.io/collector/exporter/otlpexporter v0.160.0
- gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.160.0
extensions:
- gomod: github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckextension v0.160.0
Mỗi entry có dạng gomod: <đường dẫn module> <version>; manifest còn nhận connectors và providers. Ghim version module để build tái lập được.
Thang stability của component Collector
Mỗi component mang một nhãn stability riêng — tài liệu này ghi kèm nhãn đó ở hầu hết bảng phía dưới. Thang gồm sáu mức, định nghĩa trong component-stability.md của dự án:
| Mức | Maintainer cam kết gì | Breaking change được phép ở đâu | Hàm ý cho production |
|---|---|---|---|
| Unmaintained | Không còn code owner hoạt động | Bất cứ lúc nào; component có thể bị đánh dấu deprecated rồi gỡ | Tránh. Đang dùng thì chuẩn bị đường thoát ngay |
| Deprecated | Đã có lịch gỡ, thường kèm component thay thế | Chỉ còn nhận sửa lỗi nghiêm trọng | Chỉ dùng để câu giờ trong lúc migrate |
| Development | Chưa hoàn chỉnh, có thể chưa nằm trong distro nào | Mọi thứ, kể cả tên component | Không dùng — ví dụ queuebatch ở mục 7.6 |
| Alpha | Chạy được, đã có người dùng thật | Tên khoá cấu hình và hành vi có thể đổi ở bất kỳ release nào, không bắt buộc có giai đoạn ân hạn | Dùng được nếu ghim version image và chịu đọc CHANGELOG mỗi lần nâng |
| Beta | Chạy được, cấu hình đã tương đối ổn định | Vẫn có thể đổi, nhưng phải ghi trong CHANGELOG và thường có giai đoạn deprecate trước | Mức phổ biến nhất của component đang chạy production hiện nay |
| Stable | Giữ nguyên cấu hình và hành vi | Chỉ ở major release, theo quy trình deprecate dài | Nâng version trong cùng major thì yên tâm |
Điểm mấu chốt cần nhớ: từ Alpha trở lên là component đã dùng được ở production, nhưng cấu hình có thể đổi ngay ở một minor release. Đó chính là lý do tài liệu này nhắc đi nhắc lại những thay đổi kiểu filter chuyển từ traces: span: sang trace_conditions (mục 7.6), match_once bị gỡ ở v0.120.0 (mục 7.9), invert_match đã deprecated (mục 7.11). Nâng version Collector là thao tác có rủi ro: đọc CHANGELOG rồi chạy validate trước (mục 7.4), đừng nâng mù.
Trả lời thẳng câu hỏi hay gặp — "tail_sampling là beta, spanmetrics là alpha, có được dùng ở production không?": được. Cả hai đều không có bản thay thế stable và đều đang chạy ở rất nhiều nơi. Đổi lại phải ghim đúng tag version của image (không bao giờ :latest), và mỗi lần nâng thì rà lại đúng những khối cấu hình của các component đó thay vì rà cả file.
Ba thang stability trong bộ tài liệu này độc lập với nhau, đừng suy từ cái này ra cái kia: thang component Collector ở đây; thang semantic conventions (Stable / Release Candidate / Development / Deprecated) ở Phần 9; thang package OTel JS (dòng stable 2.x so với dòng experimental 0.x) ở Phần 2. Một component Stable vẫn có thể phát ra attribute còn ở mức Development.
7.3 Deployment patterns
| Pattern | Khi nào dùng | Ưu | Nhược |
|---|---|---|---|
| (1) Không Collector — SDK → backend | POC, dev local, một service một backend | Ít thành phần nhất | Đổi backend phải deploy lại app; không tail sampling; không xoá PII tập trung; nhiều kết nối ra ngoài |
| (2) Agent mỗi node (DaemonSet) hoặc sidecar | Cần metadata node/pod, giảm tải cho app, thu thêm log file / host metrics | Độ trễ thấp (localhost); k8sattributes lấy metadata từ connection; hỏng một agent chỉ ảnh hưởng một node |
Số instance bằng số node → tốn RAM tổng; không tail sampling được vì mỗi node chỉ thấy một phần trace |
| (3) Gateway tập trung (Deployment + Service) | Cần tail sampling, spanmetrics, fan-out nhiều backend, một chỗ giữ credential | Batch lớn nhất, một chỗ cấu hình, dễ kiểm soát egress | Single point of failure nếu không HA; app chịu độ trễ mạng tới gateway; scale phải cẩn thận với component stateful |
| (4) Hai tầng: agent + gateway | Mặc định cho production Kubernetes có quy mô | Agent lo metadata + buffer cục bộ; gateway lo sampling + fan-out; app không chạm backend | Hai lớp phải vận hành và giám sát; cần loadbalancing giữa hai tầng khi gateway stateful |
7.4 Cấu trúc file cấu hình
File YAML có sáu khối top-level: receivers, processors, exporters, connectors, extensions, service. Năm khối đầu chỉ khai báo component và đặt tên cho chúng; khối service mới kích hoạt chúng.
receivers:
otlp: # tên component: <type> hoặc <type>/<tên riêng>
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch: { }
batch/traces_only: { }
exporters:
otlp/jaeger:
endpoint: jaeger:4317 # exporter otlp bắt buộc có endpoint
tls: { insecure: true }
extensions:
health_check: { } # khai báo ở đây...
service:
extensions: [health_check] # ...và phải liệt kê lại ở đây mới chạy
pipelines:
traces:
receivers: [otlp]
processors: [batch] # batch/traces_only KHÔNG chạy vì không có ở đây
exporters: [otlp/jaeger]
telemetry: { } # telemetry nội bộ của chính Collector
Lỗi cấu hình phổ biến nhất là khai báo một component rồi quên đưa nó vào pipeline. Collector vẫn khởi động, không báo lỗi, và processor xoá PII bạn vừa viết đơn giản là không chạy. Quy tắc kiểm tra: mọi tên trong receivers/processors/exporters/connectors phải xuất hiện lại trong ít nhất một pipeline dưới service.pipelines, và mọi tên trong extensions phải có trong service.extensions; kiểm nhanh bằng /debug/pipelinez của zPages (mục 7.12). Chiều ngược lại thì ồn ào hơn — tên có trong service mà thiếu khối khai báo làm Collector chết lúc khởi động: service::extensions: references extension "health_check" which is not configured.
Tên pipeline có dạng <signal> hoặc <signal>/<tên riêng>, signal là traces/metrics/logs. Receiver và exporter dùng chung một instance cho nhiều pipeline, nhưng mỗi pipeline luôn có instance processor riêng dù trỏ về cùng một tên cấu hình — state của batch hay tail_sampling không dùng chung giữa hai pipeline.
Kiểm chứng config trước khi deploy
Vì lỗi phổ biến nhất ở trên không làm Collector kêu, phải chủ động kiểm chứng chứ không chờ Collector báo. Bốn bước, xếp từ rẻ tới đắt.
(1) validate để bắt lỗi tĩnh. Mọi distro đều có subcommand này: nó đọc config, dựng component rồi thoát mà không chạy pipeline nào.
otelcol-contrib validate --config=/etc/otelcol/config.yaml # bản contrib
otelcol validate --config=/etc/otelcol/config.yaml # bản core
# Chạy thẳng trong image, không cần cài binary lên máy
docker run --rm -v "$PWD/collector-config.yaml:/tmp/config.yaml:ro" \
otel/opentelemetry-collector-contrib:0.160.0 \
validate --config=/tmp/config.yaml
Nó bắt được: YAML sai cú pháp, khoá sai kiểu dữ liệu, khoá không tồn tại, và quan trọng nhất là component không có trong distro đang chạy — đưa config có tail_sampling vào otelcol core sẽ ra error decoding 'processors': unknown type: "tail_sampling". Nó không bắt được component khai báo mà quên đưa vào pipeline, cũng không kiểm tra được endpoint có tới nơi hay không. Vì vậy sau khi Collector chạy vẫn phải đối chiếu /debug/pipelinez (mục 7.12) để xem pipeline thực tế có đúng như mình nghĩ.
(2) Chạy thử cục bộ với dữ liệu thật. Dựng Collector bằng chính config đang xét, bắn vài span vào bằng telemetrygen, để một debug exporter xác nhận dữ liệu đi hết đường:
docker run --rm -p 4317:4317 -p 13133:13133 \
-v "$PWD/collector-config.local.yaml:/tmp/config.yaml:ro" \
otel/opentelemetry-collector-contrib:0.160.0 --config=/tmp/config.yaml
# Terminal khác
go install github.com/open-telemetry/opentelemetry-collector-contrib/cmd/telemetrygen@latest
telemetrygen traces --otlp-endpoint localhost:4317 --otlp-insecure --traces 5
Dùng bản config local ở mục 7.11 cho bước này. Chạy một mình không kèm Jaeger thì otlp/jaeger sẽ liên tục báo lỗi export — kệ nó, thứ cần xác nhận là debug có in span ra hay không. Không thấy gì nghĩa là dữ liệu bị chặn ở đâu đó giữa đường: thêm tạm debug vào exporters của từng pipeline để khoanh vùng. Trên PowerShell thay $PWD bằng ${PWD}.
(3) Collector KHÔNG hot-reload config. Sửa ConfigMap rồi ngồi chờ là chờ mãi: kubelet có cập nhật file trong volume sau vài chục giây, nhưng tiến trình Collector không đọc lại file đó. Phải restart pod. Cách tự động hoá chuẩn là gắn hash nội dung ConfigMap vào annotation của podTemplate, để mỗi lần config đổi là Deployment sinh revision mới và rolling restart tự chạy:
spec:
template:
metadata:
annotations:
# Helm sinh giá trị này bằng:
# {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
# Không dùng Helm thì tự tính rồi patch vào:
# kubectl create configmap otel-gateway-config --from-file=config.yaml \
# --dry-run=client -o yaml | sha256sum
checksum/config: "3b2c1f0e9d8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c"
Không có annotation thì sau mỗi lần kubectl apply ConfigMap phải kubectl rollout restart deployment/otel-gateway bằng tay — quên bước này là nguồn gốc kinh điển của cả buổi chiều gỡ rối "config mới không có tác dụng".
(4) Rollout an toàn. Đưa một replica canary trước, giữ nguyên phần còn lại, theo dõi 10 phút: otelcol_receiver_accepted_spans phải khác 0 và tăng đều, otelcol_exporter_send_failed_spans phải phẳng, log không lặp lại dòng Exporting failed. Đạt thì rollout hết. Riêng gateway có tail_sampling: đặt terminationGracePeriodSeconds lớn hơn decision_wait cộng thời gian rút hết sending_queue, nếu không mỗi lần rolling update sẽ cắt mất đúng số trace đang chờ quyết định (mục 7.13 và Phần 10).
7.5 Receivers
| Receiver | Distro | Nhận gì | Ghi chú |
|---|---|---|---|
otlp |
core, contrib, k8s, otlp | OTLP/gRPC (4317) và OTLP/HTTP (4318) | Stable cho traces/metrics/logs. Đây là receiver bạn cần cho app Node.js |
prometheus |
core, contrib, k8s | Scrape endpoint /metrics |
Dùng cấu hình scrape kiểu Prometheus |
filelog |
contrib, k8s | Đọc log file, kể cả log container trên node | Cần chạy dạng DaemonSet mount /var/log |
hostmetrics |
core, contrib, k8s | CPU, memory, disk, network của node | Chỉ có nghĩa khi chạy agent trên node |
k8s_cluster |
contrib, k8s | Metric và event ở cấp cluster | Chỉ chạy MỘT instance, nếu không sẽ đếm trùng |
Cấu hình otlp — chú ý endpoint mặc định là localhost:4317 / localhost:4318, tức không nhận được kết nối từ ngoài container; trong Kubernetes phải đổi sang 0.0.0.0:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 8 # mặc định 4 MiB
http:
endpoint: 0.0.0.0:4318
# CORS: bắt buộc nếu có browser gửi thẳng OTLP/HTTP (web SDK)
cors:
allowed_origins:
- https://shop.example.vn
- https://*.example.vn
allowed_headers: [traceparent, tracestate, baggage, content-type]
max_age: 7200 # giây, cache preflight OPTIONS
Không có cors, browser bị chặn ở bước preflight và bạn thấy lỗi trong console chứ không thấy lỗi nào ở Collector. Chi tiết giao thức OTLP và biến môi trường phía client: xem Phần 6.
Bảo vệ receiver OTLP
Receiver OTLP mặc định không xác thực gì cả: ai tới được cổng 4317/4318 cũng bơm được span rác, khai khống được service.name của team khác, hoặc đẩy payload đủ lớn để gây OOM. Gắn authenticator (mục 7.10) — khoá auth nằm trong từng protocol, không ở cấp otlp — và đặt trần kích thước ngay tại receiver. Các trần này chặn kích thước chứ không chặn tốc độ; memory_limiter là phanh cuối nhưng là phanh toàn cục, một client hỗn kích hoạt nó thì mọi client khác cùng bị từ chối, nên hạn mức theo nguồn phải đặt ở ingress hoặc API gateway phía trước. Giữa agent và gateway thì mTLS gọn hơn token: tls.cert_file + tls.key_file + tls.client_ca_file ở receiver, phía exporter bỏ insecure: true.
extensions:
bearertokenauth/ingest:
scheme: Bearer
filename: /etc/otel/ingest-token # đọc từ file (mount Secret) để xoay token không cần sửa config
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
auth: { authenticator: bearertokenauth/ingest }
max_recv_msg_size_mib: 8
max_concurrent_streams: 64
http:
endpoint: 0.0.0.0:4318
auth: { authenticator: bearertokenauth/ingest }
max_request_body_size: 4194304 # 4 MiB
service:
# Thiếu dòng này thì receiver báo lỗi authenticator không tồn tại
extensions: [bearertokenauth/ingest]
Phía app, token đi kèm mọi request qua OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <token> (Phần 6). Endpoint nhận trace từ browser buộc phải public và token nhúng trong JS thì ai cũng đọc được — cho nó một receiver riêng trên cổng riêng, chỉ HTTP, CORS liệt kê đúng origin (* trần thì không bao giờ), body size nhỏ, đi vào pipeline riêng có một transform ghi đè service.name theo cổng đã nhận thay vì tin giá trị client khai.
7.6 Processors
Processor chạy tuần tự theo đúng thứ tự liệt kê trong pipeline — đây là một chuỗi, không phải một tập hợp; đặt sai thứ tự thì processor sau nhìn thấy dữ liệu chưa được chuẩn bị.
| Processor | Tác dụng | Lưu ý vận hành |
|---|---|---|
memory_limiter |
Từ chối dữ liệu mới khi RAM vượt ngưỡng mềm/cứng | Phải đặt ĐẦU TIÊN — đặt sau thì dữ liệu đã bị copy/parse qua các processor trước và OOM vẫn xảy ra. Tạo backpressure ngược lên receiver để client retry thay vì Collector bị kill. Docs khuyến nghị GOMEMLIMIT = 80% hard limit. Beta |
batch |
Gom span thành lô trước khi export | send_batch_size 8192, timeout 200ms, send_batch_max_size 0. Đặt cuối cùng. Beta |
resourcedetection |
Thêm resource attribute từ môi trường (env, system, cloud provider) | override mặc định true nên sẽ đè giá trị app đã đặt; đặt false nếu muốn giữ. Beta |
attributes |
insert / update / upsert / delete / hash / extract / convert trên attribute | Gọn hơn transform cho việc một dòng. Cảnh báo chính thức: sửa attribute của metric có thể tạo identity conflict vì processor không re-aggregate. Beta |
filter |
Drop hẳn span/metric/log khớp điều kiện | Cú pháp hiện hành là trace_conditions / metric_conditions / log_conditions; cú pháp cũ traces: span: đã deprecated. Alpha |
transform |
Sửa telemetry bằng OTTL | Công cụ vạn năng cho chuẩn hoá attribute và xoá PII. error_mode mặc định ignore. Beta |
k8sattributes |
Gắn k8s.namespace.name, k8s.pod.name, k8s.deployment.name, k8s.node.name… |
Stable cho traces/metrics/logs (semconv 1.42.0). Cần RBAC get/watch/list trên pods, namespaces, thêm replicasets/deployments tuỳ metadata. Phải đứng trước batch vì nó dùng thông tin connection để nhận diện pod |
redaction |
Che hoặc băm giá trị attribute theo allowed_keys, blocked_key_patterns, blocked_values |
Beta cho traces, alpha cho logs/metrics. Mask bằng * hoặc hash (hash_function: md5, sha1, sha3, hmac-sha256, hmac-sha512) |
probabilistic_sampler |
Head-based sampling ngay tại Collector | mode: hash_seed (mặc định), proportional, equalizing. Ghi threshold vào entry ot của tracestate. Beta |
tail_sampling |
Quyết định giữ trace sau khi đã thấy (gần) toàn bộ trace | Không có trong distro core — chỉ contrib và k8s. Stateful: xem 7.13. Beta |
groupbytrace |
Gom span cùng trace ID lại trước khi chuyển tiếp | wait_duration 1s, num_traces 1000000. Không đặt batch trước nó. tail_sampling đã gom tương tự nên hiếm khi cần cả hai. Beta |
Thứ tự khuyến nghị cho pipeline traces:
memory_limiter → k8sattributes → resourcedetection → redaction/transform → filter → tail_sampling → batch
Lý do: giới hạn RAM trước mọi thứ; gắn metadata khi còn thông tin connection; xoá PII trước khi dữ liệu nhân bản ra nhiều exporter; drop rác trước khi tốn RAM giữ trace cho tail sampling; batch cuối cùng để lô đi ra đã hoàn chỉnh.
batch processor hay batching trong exporter
batch processor chưa bị deprecated — README vẫn ghi stability beta và nó vẫn có trong core/contrib/k8s. Song song, exporterhelper (dùng chung cho hầu hết exporter) đã hỗ trợ batching ngay trong sending_queue qua batch.flush_timeout, batch.min_size, batch.max_size, sizer; component kế nhiệm queuebatch trong core mới ở mức development và chưa nằm trong distro nào — không dùng cho production.
Khuyến nghị: cần bền dữ liệu khi restart thì batching + queue ở exporter (sending_queue kèm file_storage), vì chỉ batch nằm sau queue mới được persist; dùng batch processor khi cần gom trước một processor tốn CPU, hoặc khi version đang chạy chưa có batching trong sending_queue. Đừng bật cả hai cho cùng một mục đích — hai tầng batching làm độ trễ cộng dồn khó suy luận.
7.7 OTTL — OpenTelemetry Transformation Language
OTTL là ngôn ngữ dùng trong transform, filter, policy ottl_condition của tail_sampling, và routing connector. Một statement gồm một function biến đổi telemetry, và tuỳ chọn một mệnh đề where quyết định function có chạy hay không:
set(span.attributes["test"], "pass") where span.attributes["test"] == nil
Đường dẫn dữ liệu luôn có prefix context: resource.attributes[...], span.name, span.attributes[...], spanevent.name, log.body, metric.name, datapoint.attributes[...]. Không trộn context khác signal trong một statement. transform tự suy ra context từ prefix nên thường không cần khai báo context.
Editor hay dùng: set, delete_key, delete_matching_keys, keep_keys, replace_pattern, truncate_all, limit, merge_maps. Converter hay dùng: IsMatch, Substring, Len, Concat, Split, SHA256, ConvertCase, ParseJSON.
processors:
transform/pii:
error_mode: ignore
trace_statements:
# 1) Xoá thẳng các attribute chứa PII do dev tự thêm
- delete_matching_keys(span.attributes, "^(app\\.(email|phone|id_card)|.*card_number)$")
# 2) Băm user id thay vì xoá — vẫn nhóm được theo người dùng mà không lộ danh tính
- set(span.attributes["app.user_hash"], SHA256(span.attributes["app.user_id"])) where span.attributes["app.user_id"] != nil
- delete_key(span.attributes, "app.user_id")
# 3) Hạ bớt độ dài db.query.text (query dài làm phình storage)
- set(span.attributes["db.query.text"], Substring(span.attributes["db.query.text"], 0, 500)) where Len(span.attributes["db.query.text"]) > 500
# 4) Chuẩn hoá attribute cũ sang tên semconv mới (xem Phần 9)
- set(span.attributes["http.request.method"], span.attributes["http.method"]) where span.attributes["http.method"] != nil
- delete_key(span.attributes, "http.method")
- set(span.attributes["url.full"], span.attributes["http.url"]) where span.attributes["http.url"] != nil
- delete_key(span.attributes, "http.url")
# 5) Gán lại service.name theo quy ước <namespace>-<service>
- set(resource.attributes["service.name"], Concat([resource.attributes["k8s.namespace.name"], resource.attributes["service.name"]], "-")) where resource.attributes["k8s.namespace.name"] != nil
# Drop span của health check — dùng filter, không dùng transform
filter/healthcheck:
error_mode: ignore
trace_conditions:
- IsMatch(span.attributes["url.path"], "^/(healthz|readyz|livez|metrics)$")
All rights reserved