← 中文资源

简体中文版本

OpenTelemetry

OpenTelemetry 是生成、采集与导出 trace、指标与日志的厂商中立标准与工具集。

阅读英文原文

概念定义

OpenTelemetry(OTel)是 CNCF 项目,为遥测——链路、指标与日志——定义统一的数据模型与传输协议,并提供各主流语言的 SDK 以及负责接收、处理、导出数据的 collector。只需面向 OTel API 埋点,数据便可发往任何兼容后端,从而把埋点与存储展示的厂商解耦。上下文传播把三类信号串在一起。

为什么重要

可观测厂商会更换、定价会变化,但嵌在应用代码里的埋点重写代价高昂。OTel 打破这种耦合:埋点一次,改一个 collector 配置就能换厂商。三类信号共用一套模型,还意味着一个 span 能携带 trace id,把它与同一次请求的确切日志行和指标尖峰关联起来。

如何落地

先用自动埋点以低成本获得 HTTP、数据库与 RPC 调用的链路,再只围绕需要被看见的业务逻辑手工加 span。把 collector 作为唯一导出点,在其中做基于尾部的采样,并保持资源属性一致以便按服务聚合。尽早确立命名约定,因为一旦各团队各造指标名,信号质量会迅速下滑。

代码示例

import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';

const sdk = new NodeSDK({
  serviceName: 'orders-service',
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTLP_ENDPOINT ?? 'http://collector:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

await sdk.start();
const tracer = trace.getTracer('orders', '1.0.0');

async function processOrder(orderId: string): Promise<void> {
  return tracer.startActiveSpan('order.process', async (span) => {
    span.setAttribute('order.id', orderId);
    try {
      await tracer.startActiveSpan('order.validate', async (child) => {
        await new Promise((r) => setTimeout(r, 10));
        child.setAttribute('validation.rules', 12);
        child.end();
      });

      await tracer.startActiveSpan('order.charge', async (child) => {
        await new Promise((r) => setTimeout(r, 25));
        child.recordException(new Error('card declined simulated'));
        child.setStatus({ code: SpanStatusCode.ERROR });
        child.end();
      });

      span.setStatus({ code: SpanStatusCode.OK });
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
    } finally {
      span.end();
    }
  });
}

await processOrder('ord-7');
await sdk.shutdown();
const HEX = { trace: 32, span: 16 };

export function parseTraceparent(value) {
  if (typeof value !== 'string') throw new TypeError('traceparent must be a string');
  const parts = value.trim().split('-');
  if (parts.length !== 4) throw new SyntaxError('traceparent must have 4 fields');
  const [version, traceId, spanId, flags] = parts;
  if (!/^[0-9a-f]{2}$/.test(version)) throw new SyntaxError('invalid version: ' + version);
  if (version === 'ff') throw new SyntaxError('version ff is invalid');
  if (!new RegExp('^[0-9a-f]{' + HEX.trace + '}$').test(traceId)) {
    throw new SyntaxError('invalid trace id: ' + traceId);
  }
  if (traceId === '0'.repeat(HEX.trace)) throw new SyntaxError('trace id must not be all zeros');
  if (!new RegExp('^[0-9a-f]{' + HEX.span + '}$').test(spanId)) {
    throw new SyntaxError('invalid span id: ' + spanId);
  }
  if (spanId === '0'.repeat(HEX.span)) throw new SyntaxError('span id must not be all zeros');
  if (!/^[0-9a-f]{2}$/.test(flags)) throw new SyntaxError('invalid flags: ' + flags);
  return { version, traceId, spanId, sampled: (parseInt(flags, 16) & 1) === 1 };
}