0

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.systemrpc.grpc.status_code đã deprecated, thay bằng rpc.system.namerpc.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. maxQueueSize mặc định 2048 sẽ tràn và span bị drop im lặng (Phần 5A, Phần 11). Hoặc nâng maxQueueSize/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.idenduser.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_idspan_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() trong afterEach; 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ông BatchSpanProcessor: 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.parentSpanIdspan.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:

  1. ClientpropagateTraceHeaderCorsUrls trong khối trên quyết định origin nào được nhận header traceparent. 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.
  2. Server APItraceparent là custom header nên trình duyệt gửi preflight OPTIONS; API phải trả Access-Control-Allow-Headers: traceparent, tracestate (thêm baggage nếu dùng). Thiếu dòng này thì request bị chặn hoặc header bị strip (Phần 3).
  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ó transform ghi đè service.name theo 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 mang service.name của team khác (Phần 7).
  • Không tin traceparent do client gửi ở rìa public: sampled flag 01 từ 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 (TraceIdRatioBasedSampler truyền vào WebTracerProvider) — Phần 4.
  • Giới hạn span resource của DocumentLoadInstrumentation nế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: 1 INTERNAL, 2 SERVER, 3 CLIENT, 4 PRODUCER, 5 CONSUMER (0 = UNSPECIFIED). status.code: 0 UNSET, 1 OK, 2 ERROR.
  • 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ự đặt OK. Copy payload này làm mẫu thì đừng thêm "status": { "code": 1 }. Xem Phần 1 và Phần 9.
  • flags là bit-field: 8 bit thấp là W3C trace-flags (0x01 = sampled), 0x100 báo "đã biết parent có remote hay không", 0x200 báo parent là remote. Giá trị 769 = 0x301 nghĩ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. intValue cũng là chuỗi (proto3 JSON mapping cho int64).
  • Trong OTLP/JSON, traceIdspanId là 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. Trong http/protobufgrpc chú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/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_successrejected_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.

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 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 503 cũng có thể kèm Retry-After
Kết nối đứt, không có response 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 dump process.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_HEADERS chứ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=true chỉ 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_ENDPOINTbase URL, SDK nối thêm v1/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 (jaegerottrace 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-endpointlocalhost: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_spansotelcol_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 connectorsproviders. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí