插桩

OpenTelemetry JavaScript 插桩工具

插桩是指向应用中添加可观测性代码的行为。

如果你正在对一个应用进行插桩,需要使用适合你语言的 OpenTelemetry SDK。然后,你可以使用 SDK 初始化 OpenTelemetry,并使用 API 对代码进行插桩。这将从你的应用及其安装的任何带有插桩的库中导出遥测数据。

如果你正在对一个库进行插桩,只需安装适合你语言的 OpenTelemetry API 包。你的库不会自行导出遥测数据。 只有当该库作为使用 OpenTelemetry SDK 的应用的一部分时,它才会导出遥测数据。 有关如何对库进行插桩的更多信息,请参见插桩库

有关 OpenTelemetry API 和 SDK 的更多信息,请参见规范

示例应用程序准备

本页使用了入门指南中的示例应用程序的修改版本,帮助你学习手动插桩。

你无需使用该示例应用程序:如果你希望为自己的应用程序或库实现插桩,可遵循此处的说明,将该流程调整适配到你自己的代码中。

依赖项

在一个新目录下创建一个空的 NPM package.json 文件:

npm init -y

然后,安装 Express 依赖项。

npm install express @types/express
npm install -D tsx  # 一款可通过 Node.js 直接运行 TypeScript(.ts)文件的工具
npm install express

创建和启动一个 HTTP 服务器

为了突出类库和独立应用程序实现插桩的区别, 将掷骰子逻辑提取到一个类库文件中,该文件将作为应用程序文件的依赖项导入。

创建名为 dice.ts(或 dice.js 如果不使用 TypeScript)的库文件,并添加以下代码:

/*dice.ts*/
function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function rollTheDice(rolls: number, min: number, max: number) {
  const result: number[] = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}
/*dice.js*/
function rollOnce(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function rollTheDice(rolls, min, max) {
  const result = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}

module.exports = { rollTheDice };

创建名为 app.ts(或 app.js 如果不使用 TypeScript)的应用程序文件,并添加以下代码:

/*app.ts*/
import express, { type Express } from 'express';
import { rollTheDice } from './dice';

const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("Request parameter 'rolls' is missing or not a number.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});
/*app.js*/
const express = require('express');
const { rollTheDice } = require('./dice.js');

const PORT = parseInt(process.env.PORT || '8080');
const app = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("Request parameter 'rolls' is missing or not a number.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});

为了确保其工作正常,请使用以下命令运行应用程序, 并在浏览器中打开 http://localhost:8080/rolldice?rolls=12

$ npx tsx app.ts
Listening for requests on http://localhost:8080
$ node app.js
Listening for requests on http://localhost:8080

手动插桩配置

依赖项

安装 OpenTelemetry API 包:

npm install @opentelemetry/api @opentelemetry/resources @opentelemetry/semantic-conventions

初始化 SDK

若要为 Node.js 应用实现插桩,请安装 OpenTelemetry Node.js 版 SDK

npm install @opentelemetry/sdk-node

在应用程序加载任何其他模块之前,必须初始化 SDK。 若 SDK 初始化失败或初始化时机过晚, 任何从该 API 获取追踪器(tracer)或计量器(meter)的类库,都会被提供无操作实现版本。

/*instrumentation.ts*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import {
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'yourServiceName',
    [ATTR_SERVICE_VERSION]: '1.0',
  }),
  traceExporter: new ConsoleSpanExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
});

sdk.start();
/*instrumentation.mjs*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import {
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'dice-server',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
  traceExporter: new ConsoleSpanExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
});

sdk.start();

出于调试和本地开发目的,以下示例将遥测数据导出到控制台。 完成手动插桩的配置后,你需要配置一个合适的导出器,以将应用的遥测数据导出至一个或多个遥测后端。

该示例还设置了强制性的 SDK 默认属性 service.name,它保存服务的逻辑名称, 并设置了可选的(但强烈建议!)属性 service.version,它保存服务 API 或实现的版本。

设置资源属性还存在其他方法。如需了解更多信息,请参见资源

npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js

此基本设置尚未对应用程序产生任何影响。 你需要添加链路指标日志的代码。

你可以将类库注册到 OpenTelemetry Node.js 版 SDK 中,以便为你的依赖项生成遥测数据。 如需了解更多信息,请参见类库

链路

初始化链路追踪

要在应用程序中启用链路追踪, 你需要有一个已初始化的 TracerProvider, 它将允许你创建一个 Tracer

如果未创建 TracerProvider,则 OpenTelemetry 链路追踪 API 将使用无操作实现版本,无法生成数据。 如下文所述,请修改 instrumentation.ts(或 instrumentation.js)文件, 将所有 SDK 初始化代码纳入其中,以适配 Node 环境和浏览器环境。

Node.js

如果你遵循了上述初始化 SDK 的说明, 你已经有一个为你设置好的 TracerProvider。 你可以继续获取一个 tracer

浏览器

首先,确保你拥有正确的包:

npm install @opentelemetry/sdk-trace-web

然后,更新 instrumentation.ts(或 instrumentation.js), 使其包含所有 SDK 初始化代码:

import {
  defaultResource,
  resourceFromAttributes,
} from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'service-name-here',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);

const provider = new WebTracerProvider({
  resource: resource,
  spanProcessors: [processor],
});

provider.register();
const opentelemetry = require('@opentelemetry/api');
const {
  defaultResource,
  resourceFromAttributes,
} = require('@opentelemetry/resources');
const {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');
const { WebTracerProvider } = require('@opentelemetry/sdk-trace-web');
const {
  ConsoleSpanExporter,
  BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'service-name-here',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);

const provider = new WebTracerProvider({
  resource: resource,
  spanProcessors: [processor],
});

provider.register();

你需要将此文件与你的 Web 应用程序捆绑在一起,来确保在应用程序启动时加载并执行此文件。

这尚未对你的应用程序产生任何影响: 你需要创建 Span,才能使你的应用程序生成遥测数据。

选择正确的 Span 处理器

默认情况下,Node SDK 使用 BatchSpanProcessor,Web SDK 示例中也选择了此 Span 处理器。 BatchSpanProcessor 会在导出之前批量处理 Span。 对于应用程序而言,这通常是适用的处理器。

相比之下,SimpleSpanProcessor 会在 Span 创建时立即处理 Span。 这意味着如果你创建了 5 个 Span, 每个 Span 都会立即被处理和导出,而不会等待其他 Span 的创建。 在你不想承担批次数据丢失风险的场景下,或是在开发环境中试用 OpenTelemetry 时,这一方式会十分实用。 但是,这也可能带来显著的开销,尤其是在通过网络导出 Span 的情况下—— 每个 Span 创建时都需要进行网络调用,这会增加应用程序的延迟。

在大多数情况下,请坚持使用 BatchSpanProcessor,而非 SimpleSpanProcessor

获取一个追踪器

在应用程序中的任何地方编写手动链路追踪代码时,都应调用 getTracer 来获取一个追踪器。

import opentelemetry from '@opentelemetry/api';
//......

const tracer = opentelemetry.trace.getTracer(
  'instrumentation-scope-name',
  'instrumentation-scope-version',
);

// 现在你可以使用 'tracer' 来实现链路追踪了!
const opentelemetry = require('@opentelemetry/api');
//......

const tracer = opentelemetry.trace.getTracer(
  'instrumentation-scope-name',
  'instrumentation-scope-version',
);

// 现在你可以使用 'tracer' 来实现链路追踪了!

instrumentation-scope-nameinstrumentation-scope-version 的值应能唯一标识插桩作用域。 例如包名、模块名或类名。其中名称为必填项,版本号虽为可选项,但仍建议填写。

一般建议在需要时调用 getTracer,而不是将 tracer 实例导出到应用程序的其他部分。 这有助于避免在其他依赖项存在时导致的加载问题。

示例应用为例,存在两处可通过恰当的插桩作用域获取追踪器的场景:

首先,在应用程序文件 app.ts(或 app.js)中:

/*app.ts*/
import { trace } from '@opentelemetry/api';
import express, { type Express } from 'express';
import { rollTheDice } from './dice';

const tracer = trace.getTracer('dice-server', '0.1.0');

const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("Request parameter 'rolls' is missing or not a number.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});
/*app.js*/
const { trace } = require('@opentelemetry/api');
const express = require('express');
const { rollTheDice } = require('./dice.js');

const tracer = trace.getTracer('dice-server', '0.1.0');

const PORT = parseInt(process.env.PORT || '8080');
const app = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("Request parameter 'rolls' is missing or not a number.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});

And second, in the library file dice.ts (or dice.js):

/*dice.ts*/
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('dice-lib');

function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function rollTheDice(rolls: number, min: number, max: number) {
  const result: number[] = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}
/*dice.js*/
const { trace } = require('@opentelemetry/api');

const tracer = trace.getTracer('dice-lib');

function rollOnce(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function rollTheDice(rolls, min, max) {
  const result = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}

module.exports = { rollTheDice };

创建Span

现在你已经有了已初始化的追踪器, 你可以使用它来创建 Span

OpenTelemetry JavaScript API 提供了两种创建 Span 的方法:

  • tracer.startSpan: 启动一个新 Span,但不会将其设置到上下文中。
  • tracer.startActiveSpan: 启动一个新 Span 并调用给定的回调函数,将新 Span 作为第一个参数传递。 新 Span 会被设置到上下文中,并且在回调函数执行期间会被激活。

在大多数情况下,你应该使用后者(tracer.startActiveSpan),因为它会负责设置 Span 和其上下文激活。

下面的代码演示了如何创建一个活跃的 Span。

import { trace, type Span } from '@opentelemetry/api';

/* ...... */

export function rollTheDice(rolls: number, min: number, max: number) {
  // 创建一个 Span。该 Span 应该被关闭。
  return tracer.startActiveSpan('rollTheDice', (span: Span) => {
    const result: number[] = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(min, max));
    }
    // 确保关闭 Span!
    span.end();
    return result;
  });
}
function rollTheDice(rolls, min, max) {
  // 创建一个 Span。该 Span 应该被关闭。
  return tracer.startActiveSpan('rollTheDice', (span) => {
    const result = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(min, max));
    }
    // 确保关闭 Span!
    span.end();
    return result;
  });
}

如果你遵循了上述示例应用的说明, 并将上述代码复制到你的库文件 dice.ts(或 dice.js)中, 你应该现在能够在应用中看到生成的 Span。

按照以下方式启动应用,然后通过访问 http://localhost:8080/rolldice?rolls=12 来发送请求(使用浏览器或 curl)。

npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js

经过一段时间后,你应该会在控制台中看到 ConsoleSpanExporter 打印的 Span,类似如下内容:

{
  resource: {
    attributes: {
      'service.name': 'dice-server',
      'service.version': '0.1.0',
      // ......
    }
  },
  instrumentationScope: { name: 'dice-lib', version: undefined, schemaUrl: undefined },
  traceId: '30d32251088ba9d9bca67b09c43dace0',
  parentSpanContext: undefined,
  traceState: undefined,
  name: 'rollTheDice',
  id: 'cc8a67c2d4840402',
  kind: 0,
  timestamp: 1756165206470000,
  duration: 35.584,
  attributes: {},
  status: { code: 0 },
  events: [],
  links: []
}

创建嵌套 Span

嵌套 Span 可用于追踪具有嵌套结构的任务。 例如,下面的 rollOnce() 函数代表一个嵌套操作。 以下示例会创建一个嵌套跨度,用于追踪 rollOnce() 函数的执行过程:

function rollOnce(i: number, min: number, max: number) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span: Span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);
    span.end();
    return result;
  });
}

export function rollTheDice(rolls: number, min: number, max: number) {
  // 创建一个 Span。该 Span 应该被关闭。
  return tracer.startActiveSpan('rollTheDice', (parentSpan: Span) => {
    const result: number[] = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(i, min, max));
    }
    // 确保关闭 Span!
    parentSpan.end();
    return result;
  });
}
function rollOnce(i, min, max) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);
    span.end();
    return result;
  });
}

function rollTheDice(rolls, min, max) {
  // 创建一个 Span。该 Span 应该被关闭。
  return tracer.startActiveSpan('rollTheDice', (parentSpan) => {
    const result = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(i, min, max));
    }
    // 确保关闭 Span!
    parentSpan.end();
    return result;
  });
}

这段代码会为每一次掷骰子操作创建一个子跨度,该子跨度将 parentSpan 的 ID 作为自身的父级 ID:

{
  traceId: '6469e115dc1562dd768c999da0509615',
  parentSpanContext: {
    traceId: '6469e115dc1562dd768c999da0509615',
    spanId: '38691692d6bc3395',
    // ......
  },
  name: 'rollOnce:0',
  id: '36423bc1ce7532b0',
  timestamp: 1756165362215000,
  duration: 85.667,
  // ......
}
{
  traceId: '6469e115dc1562dd768c999da0509615',
  parentSpanContext: {
    traceId: '6469e115dc1562dd768c999da0509615',
    spanId: '38691692d6bc3395',
    // ......
  },
  name: 'rollOnce:1',
  id: 'ed9bbba2264d6872',
  timestamp: 1756165362215000,
  duration: 16.834,
  // ......
}
{
  traceId: '6469e115dc1562dd768c999da0509615',
  parentSpanContext: undefined,
  name: 'rollTheDice',
  id: '38691692d6bc3395',
  timestamp: 1756165362214000,
  duration: 1022.209,
  // ......
}

创建独立 Span

前述示例演示了如何创建活跃 Span。在某些场景下,你可能需要创建非活跃 Span,这类跨度彼此互为同级,而非呈嵌套关系。

const doWork = () => {
  const span1 = tracer.startSpan('work-1');
  // 执行一些工作
  const span2 = tracer.startSpan('work-2');
  // 执行更多工作
  const span3 = tracer.startSpan('work-3');
  // 执行更多工作

  span1.end();
  span2.end();
  span3.end();
};

在此示例中,span1span2span3 是同级 Span, 它们都不被视为当前活动 Span。 它们共用同一个父级,而非彼此嵌套。

若你有一些归为一组、但在逻辑层面彼此独立的工作单元,这种结构会十分实用。

获得当前 Span

在程序执行过程中的特定节点, 对当前或活跃 Span 执行某些操作,有时会起到事半功倍的效果。

const activeSpan = opentelemetry.trace.getActiveSpan();

// 对活跃 Span 执行某些操作,视情况可选是否结束它。

从上下文获取 Span

从给定的上下文中获取 Span 也会很有帮助,这个上下文未必是当前活跃的 Span。

const ctx = getContextFromSomewhere();
const span = opentelemetry.trace.getSpan(ctx);

// 对获取到的 Span 执行某些操作,视情况可选是否结束它。

属性

属性允许你将键值对附加到 Span 上, 以便在追踪当前操作时携带更多信息。

function rollOnce(i: number, min: number, max: number) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span: Span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);

    // 向 Span 添加属性
    span.setAttribute('dicelib.rolled', result.toString());

    span.end();
    return result;
  });
}
function rollOnce(i, min, max) {
  return tracer.startActiveSpan(`rollOnce:${i}`, (span) => {
    const result = Math.floor(Math.random() * (max - min + 1) + min);

    // 向 Span 添加属性
    span.setAttribute('dicelib.rolled', result.toString());

    span.end();
    return result;
  });
}

You can also add attributes to a span as it’s created:

tracer.startActiveSpan(
  'app.new-span',
  { attributes: { attribute1: 'value1' } },
  (span) => {
    // 执行一些工作......

    span.end();
  },
);
function rollTheDice(rolls: number, min: number, max: number) {
  return tracer.startActiveSpan(
    'rollTheDice',
    { attributes: { 'dicelib.rolls': rolls.toString() } },
    (span: Span) => {
      /* ...... */
    },
  );
}
function rollTheDice(rolls, min, max) {
  return tracer.startActiveSpan(
    'rollTheDice',
    { attributes: { 'dicelib.rolls': rolls.toString() } },
    (span) => {
      /* ...... */
    },
  );
}

语义属性

对于代表 HTTP、数据库调用等常见协议操作的 Span,存在对应的语义规范。 这些 Span 的语义规范在链路语义规范中有定义。 在本指南的简单示例中,可直接使用源代码属性。

首先,将语义规范作为依赖项添加到你的应用程序中:

npm install --save @opentelemetry/semantic-conventions

然后,将以下内容添加到应用程序文件的顶部:

import {
  ATTR_CODE_FUNCTION_NAME,
  ATTR_CODE_FILE_PATH,
} from '@opentelemetry/semantic-conventions';
const {
  ATTR_CODE_FUNCTION_NAME,
  ATTR_CODE_FILE_PATH,
} = require('@opentelemetry/semantic-conventions');

Finally, you can update your file to include semantic attributes:

const doWork = () => {
  tracer.startActiveSpan('app.doWork', (span) => {
    span.setAttribute(ATTR_CODE_FUNCTION_NAME, 'doWork');
    span.setAttribute(ATTR_CODE_FILE_PATH, __filename);

    // 执行一些工作......

    span.end();
  });
};

Span 事件

一个 Span 事件Span 上的一个便于人类阅读的消息, 表示一个无持续时长的离散事件,可以通过单个时间戳进行跟踪。你可以将其视为原始日志。

span.addEvent('Doing something');

const result = doWork();

你还可以使用额外的属性来创建 Span 事件:

span.addEvent('some log', {
  'log.severity': 'error',
  'log.message': 'Data not found',
  'request.id': requestId,
});

Span 可以零个或多个 Link 链接到其他因果相关的 Span。 一种常见场景是将一条或多条追踪链路,与当前 Span 建立关联关系。

const someFunction = (spanToLinkFrom) => {
  const options = {
    links: [
      {
        context: spanToLinkFrom.spanContext(),
      },
    ],
  };

  tracer.startActiveSpan('app.someFunction', options, (span) => {
    // 执行一些工作......

    span.end();
  });
};

Span 状态

可以在一个 Span 上设置一个 Status,通常用于指明某个 Span 没有成功完成 —— 即标记为 Error。默认情况下,所有的 Span 状态都是 Unset,意味着该操作完成但未明确表示是否出错。 如果你想显式地标记某个操作是成功的,而不是依赖默认的 Unset,就可以使用 Ok 状态。

状态可以在 Span 结束前的任何时候设置。

import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';

// ......

tracer.startActiveSpan('app.doWork', (span) => {
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    if (i > 10000) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: 'Error',
      });
    }
  }

  span.end();
});
const opentelemetry = require('@opentelemetry/api');

// ......

tracer.startActiveSpan('app.doWork', (span) => {
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    if (i > 10000) {
      span.setStatus({
        code: opentelemetry.SpanStatusCode.ERROR,
        message: 'Error',
      });
    }
  }

  span.end();
});

记录异常

当异常发生时,记录异常是一个好主意。建议将其 Span 状态结合使用。

import opentelemetry, { SpanStatusCode } from '@opentelemetry/api';

// ......

try {
  doWork();
} catch (ex) {
  if (ex instanceof Error) {
    span.recordException(ex);
  }
  span.setStatus({ code: SpanStatusCode.ERROR });
}
const opentelemetry = require('@opentelemetry/api');

// ......

try {
  doWork();
} catch (ex) {
  if (ex instanceof Error) {
    span.recordException(ex);
  }
  span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR });
}

使用 sdk-trace-base 手动传播 Span 上下文

在某些情况下,你可能无法使用 Node.js SDK 或 Web SDK。 除初始化代码外,最大的区别在于,你必须手动将 Span 设置为当前上下文中的活跃状态,才能创建嵌套跨度。

使用 sdk-trace-base 初始化追踪功能

追踪功能的初始化方式类似与你在 Node.js 或 Web SDK 中的实现方式。

import opentelemetry from '@opentelemetry/api';
import {
  CompositePropagator,
  W3CTraceContextPropagator,
  W3CBaggagePropagator,
} from '@opentelemetry/core';
import {
  BasicTracerProvider,
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';

opentelemetry.trace.setGlobalTracerProvider(
  new BasicTracerProvider({
    // 配置 Span 处理器,将 Span 发送至导出器
    spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
  }),
);

opentelemetry.propagation.setGlobalPropagator(
  new CompositePropagator({
    propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
  }),
);

// 这是我们在所有插桩代码中访问的内容
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');
const opentelemetry = require('@opentelemetry/api');
const {
  CompositePropagator,
  W3CTraceContextPropagator,
  W3CBaggagePropagator,
} = require('@opentelemetry/core');
const {
  BasicTracerProvider,
  ConsoleSpanExporter,
  BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');

opentelemetry.trace.setGlobalTracerProvider(
  new BasicTracerProvider({
    // 配置 Span 处理器,将 Span 发送至导出器
    spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
  }),
);

opentelemetry.propagation.setGlobalPropagator(
  new CompositePropagator({
    propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
  }),
);

// 这是我们在所有插桩代码中访问的内容
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');

与本文档中的其他示例一样,这会导出一个可在整个应用程序中使用的追踪器。

使用 sdk-trace-base 创建嵌套 Span

要创建嵌套跨度,你需要将当前创建的 Span 设置为当前上下文中的活跃 Span。 不要使用 startActiveSpan,因为它不会为你完成此操作。

const mainWork = () => {
  const parentSpan = tracer.startSpan('main');

  for (let i = 0; i < 3; i += 1) {
    doWork(parentSpan, i);
  }

  // 确保结束父 Span!
  parentSpan.end();
};

const doWork = (parent, i) => {
  // 要创建子 Span,我们需要将当前(父)Span 设置为当前上下文中的活跃 Span,
  // 然后使用 resulting context 创建子 Span。
  const ctx = opentelemetry.trace.setSpan(
    opentelemetry.context.active(),
    parent,
  );
  const span = tracer.startSpan(`doWork:${i}`, undefined, ctx);

  // 模拟一些随机工作。
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    // empty
  }

  // 确保结束子 Span!如果不这样做,
  // 它将继续跟踪超出 'doWork' 的工作!
  span.end();
};

所有其他 API 在使用 sdk-trace-base 时与 Node.js 或 Web SDK 相同。

指标

指标会将单独的测量数据聚合为汇总数据, 并生成不受系统负载影响的稳定数据。 聚合值缺少诊断低级问题所需的详细信息,但通过帮助识别趋势和提供应用程序运行时遥测, 补充了跨度。

初始化指标工具

要在应用程序中启用指标工具, 你需要初始化一个 MeterProvider, 该提供程序将允许你创建一个 Meter

如果未创建 MeterProvider,OpenTelemetry 指标 API 将使用无操作实现,无法生成数据。 如接下来所述,修改 instrumentation.ts(或 instrumentation.js)文件, 将所有 SDK 初始化代码包含在 Node 和浏览器中。

Node.js

如果你遵循了上述初始化 SDK 的说明, 则已为你设置了一个 MeterProvider。你可以继续获取指标工具

使用 sdk-metrics 初始化指标工具

在某些情况下,你可能无法或不想使用 Node.js 完整 OpenTelemetry SDK。 若你希望在浏览器环境中使用 OpenTelemetry JavaScript 库,该要求同样有效。

若要初始化指标工具,你可以使用 @opentelemetry/sdk-metrics 包:

npm install @opentelemetry/sdk-metrics

若你尚未为追踪功能创建过该文件, 请新建一个独立的 instrumentation.ts(或 instrumentation.js)文件, 并将所有 SDK 初始化代码置于其中:

import opentelemetry from '@opentelemetry/api';
import {
  ConsoleMetricExporter,
  MeterProvider,
  PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import {
  defaultResource,
  resourceFromAttributes,
} from '@opentelemetry/resources';
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'dice-server',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const metricReader = new PeriodicExportingMetricReader({
  exporter: new ConsoleMetricExporter(),
  // 默认值为 60000ms(60 秒)。仅用于演示目的,将其设置为 10 秒。
  exportIntervalMillis: 10000,
});

const myServiceMeterProvider = new MeterProvider({
  resource: resource,
  readers: [metricReader],
});

// 将此 MeterProvider 配置为当前待插桩应用的全局实例。
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);
const opentelemetry = require('@opentelemetry/api');
const {
  MeterProvider,
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const {
  defaultResource,
  resourceFromAttributes,
} = require('@opentelemetry/resources');
const {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');

const resource = defaultResource().merge(
  resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'service-name-here',
    [ATTR_SERVICE_VERSION]: '0.1.0',
  }),
);

const metricReader = new PeriodicExportingMetricReader({
  exporter: new ConsoleMetricExporter(),

  // 默认值为 60000ms(60 秒)。仅用于演示目的,将其设置为 10 秒。
  exportIntervalMillis: 10000,
});

const myServiceMeterProvider = new MeterProvider({
  resource: resource,
  readers: [metricReader],
});

// 将此 MeterProvider 配置为当前待插桩应用的全局实例。
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);

你需要在运行应用程序时使用 --import 导入此文件,例如:

npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js

现在已配置 MeterProvider,你可以获取 Meter

获取计量器

在应用中所有手动编写了插桩代码的位置,你都可以调用 getMeter 方法来获取计量器。例如:

import opentelemetry from '@opentelemetry/api';

const myMeter = opentelemetry.metrics.getMeter(
  'instrumentation-scope-name',
  'instrumentation-scope-version',
);

// 现在你可借助该 'meter' 来创建插桩了!
const opentelemetry = require('@opentelemetry/api');

const myMeter = opentelemetry.metrics.getMeter(
  'instrumentation-scope-name',
  'instrumentation-scope-version',
);

// 现在你可借助该 'meter' 来创建插桩了!

instrumentation-scope-nameinstrumentation-scope-version 的取值应能唯一标识插桩范围, 例如包名、模块名或类名。其中名称为必填项,版本虽为可选项但仍建议填写。

通常建议在应用程序中需要时调用 getMeter,而非将计量器实例导出到应用程序的其他部分。 当涉及其他必要依赖项时,此举有助于规避更为棘手的应用程序加载问题。

示例应用为例,有两处位置可结合合适的插桩范围来获取计量器:

首先,在应用程序文件 app.ts(或 app.js)中:

/*app.ts*/
import { metrics, trace } from '@opentelemetry/api';
import express, { type Express } from 'express';
import { rollTheDice } from './dice';

const tracer = trace.getTracer('dice-server', '0.1.0');
const meter = metrics.getMeter('dice-server', '0.1.0');

const PORT: number = parseInt(process.env.PORT || '8080');
const app: Express = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("Request parameter 'rolls' is missing or not a number.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});
/*app.js*/
const { trace, metrics } = require('@opentelemetry/api');
const express = require('express');
const { rollTheDice } = require('./dice.js');

const tracer = trace.getTracer('dice-server', '0.1.0');
const meter = metrics.getMeter('dice-server', '0.1.0');

const PORT = parseInt(process.env.PORT || '8080');
const app = express();

app.get('/rolldice', (req, res) => {
  const rolls = req.query.rolls ? parseInt(req.query.rolls.toString()) : NaN;
  if (isNaN(rolls)) {
    res
      .status(400)
      .send("Request parameter 'rolls' is missing or not a number.");
    return;
  }
  res.send(JSON.stringify(rollTheDice(rolls, 1, 6)));
});

app.listen(PORT, () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});

其次,在库文件 dice.ts(或 dice.js)中:

/*dice.ts*/
import { trace, metrics } from '@opentelemetry/api';

const tracer = trace.getTracer('dice-lib');
const meter = metrics.getMeter('dice-lib');

function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function rollTheDice(rolls: number, min: number, max: number) {
  const result: number[] = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}
/*dice.js*/
const { trace, metrics } = require('@opentelemetry/api');

const tracer = trace.getTracer('dice-lib');
const meter = metrics.getMeter('dice-lib');

function rollOnce(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function rollTheDice(rolls, min, max) {
  const result = [];
  for (let i = 0; i < rolls; i++) {
    result.push(rollOnce(min, max));
  }
  return result;
}

module.exports = { rollTheDice };

现在你已初始化了计量器, 你可以借助它来创建指标插桩

使用计数器

计数器可用于测量非负、递增的值。

示例应用为例,我们可以使用它来统计骰子被掷的次数:

/*dice.ts*/
const counter = meter.createCounter('dice-lib.rolls.counter');

function rollOnce(min: number, max: number) {
  counter.add(1);
  return Math.floor(Math.random() * (max - min + 1) + min);
}
/*dice.js*/
const counter = meter.createCounter('dice-lib.rolls.counter');

function rollOnce(min, max) {
  counter.add(1);
  return Math.floor(Math.random() * (max - min + 1) + min);
}

使用增减计数器

增减计数器支持递增和递减操作,可用于观测一个可增可减的累计值。

const counter = myMeter.createUpDownCounter('events.counter');

//......

counter.add(1);

//......

counter.add(-1);

使用直方图

直方图用于测量一段时间内值的分布。

例如,以下示例展示了如何基于 Express 框架上报某一 API 路由的响应时间分布情况:

import express from 'express';

const app = express();

app.get('/', (_req, _res) => {
  const histogram = myMeter.createHistogram('task.duration');
  const startTime = new Date().getTime();

  // 在一次 API 调用中执行一些业务处理逻辑

  const endTime = new Date().getTime();
  const executionTime = endTime - startTime;

  // 记录任务操作的持续时间
  histogram.record(executionTime);
});
const express = require('express');

const app = express();

app.get('/', (_req, _res) => {
  const histogram = myMeter.createHistogram('task.duration');
  const startTime = new Date().getTime();

  // 在一次 API 调用中执行一些业务处理逻辑

  const endTime = new Date().getTime();
  const executionTime = endTime - startTime;

  // 记录任务操作的持续时间
  histogram.record(executionTime);
});

使用可观测(异步)计数器

可观测计数器可用于计量具有累加性、非负性且单调递增的数值。

const events = [];

const addEvent = (name) => {
  events.push(name);
};

const counter = myMeter.createObservableCounter('events.counter');

counter.addCallback((result) => {
  result.observe(events.length);
});

//…… 对 addEvent 方法的调用

使用可观测(异步)增减计数器

可观测增减计数器支持递增和递减操作,可用于计量具备累加性、非负性但非单调递增的累计值。

const events = [];

const addEvent = (name) => {
  events.push(name);
};

const removeEvent = () => {
  events.pop();
};

const counter = myMeter.createObservableUpDownCounter('events.counter');

counter.addCallback((result) => {
  result.observe(events.length);
});

//…… 对 addEvent 和 removeEvent 方法的调用

使用可观测(异步)瞬时值计量器

可观测瞬时值计量器用于测量非累加性值。

let temperature = 32;

const gauge = myMeter.createObservableGauge('temperature.gauge');

gauge.addCallback((result) => {
  result.observe(temperature);
});

//…… 温度变量由传感器修改

插桩说明

创建计数器、直方图等插桩时,你可以为它们添加描述信息。

const httpServerResponseDuration = myMeter.createHistogram(
  'http.server.duration',
  {
    description: 'A distribution of the HTTP server response times',
    unit: 'milliseconds',
    valueType: ValueType.INT,
  },
);

JavaScript 中,每个配置类型含义如下:

  • description - 便于人类阅读的插桩描述
  • unit - 用于描述数值所代表的计量单位。例如,使用 milliseconds 来衡量持续时间,或使用 bytes 来统计字节数。
  • valueType - 用于测量的数值类型。

通常建议描述你创建的每个插桩。

添加属性信息

你可在指标生成时为其添加属性信息。

const counter = myMeter.createCounter('my.counter');

counter.add(1, { 'some.optional.attribute': 'some value' });

配置指标视图

指标视图为开发者提供了自定义由指标 SDK 对外暴露的指标数据的能力。

选择器

要实例化一个视图,需先选定目标指标插桩。以下是适用于指标的合法选择器:

  • instrumentType
  • instrumentName
  • meterName
  • meterVersion
  • meterSchemaUrl

选择器 instrumentName(类型为字符串)支持通配符,因此你可以使用 * 选择所有插桩,或使用 http* 选择所有名称以 http 开头的插桩。

示例

对所有指标类型进行属性筛选:

const limitAttributesView = {
  // 仅导出属性 'environment'
  attributeKeys: ['environment'],
  // 应用视图到所有插桩
  instrumentName: '*',
};

删除所有 meter 名称为 pubsub 的插桩:

const dropView = {
  aggregation: { type: AggregationType.DROP },
  meterName: 'pubsub',
};

为名为 http.server.duration 的直方图定义显式的桶大小:

const histogramView = {
  aggregation: {
    type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
    options: { boundaries: [0, 1, 5, 10, 15, 20, 25, 30] },
  },
  instrumentName: 'http.server.duration',
  instrumentType: InstrumentType.HISTOGRAM,
};

关联至计量器提供器

配置视图后,将它们关联到相应的计量器提供器:

const meterProvider = new MeterProvider({
  views: [limitAttributesView, dropView, histogramView],
});

日志

日志 API 和 SDK 当前正在开发中。

下一步

你还需要配置一个合适的导出器,以将遥测数据导出至一个或多个遥测后端。