仪表化

OpenTelemetry JavaScript 的遥测

Instrumention 是指您自己向应用程序添加可观察性代码的过程。

如果您正在 instrumenting 应用程序,您需要为您使用的语言使用 OpenTelemetry SDK。然后,您将使用 SDK 初始化 OpenTelemetry,并使用 API instrument 您的代码。这将从您的应用程序和任何也带有 instrumention 的已安装库中发出遥测数据。

如果您正在 instrumenting 库,请仅安装您语言的 OpenTelemetry API 包。您的库不会自行发出遥测数据。只有当它作为使用 OpenTelemetry SDK 的应用程序的一部分时,它才会发出遥测数据。有关 instrumenting 库的更多信息,请参阅 Libraries

有关 OpenTelemetry API 和 SDK 的更多信息,请参阅 specification

示例应用程序准备

本页使用了一个修改版的示例应用程序,来自入门指南,以帮助您学习手动遥测。

您不必使用示例应用程序:如果您想遥测自己的应用程序或库,请按照这里的说明调整该过程以适应您自己的代码。

依赖项

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

npm init -y

接下来,安装 Express 依赖项。

npm install express @types/express
npm install -D tsx  # a tool to run TypeScript (.ts) files directly with node
npm install express

创建并启动 HTTP 服务器

为了突出遥测*库*和独立*应用程序*之间的区别,将掷骰子功能拆分到一个*库文件*中,该文件随后将被*应用程序文件*作为依赖项导入。

创建名为 dice.ts(如果您不使用 TypeScript,则为 dice.js)的*库文件*,并将以下代码添加到其中

/*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(如果您不使用 TypeScript,则为 app.js)的*应用程序文件*,并将以下代码添加到其中

/*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 https://:${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 https://:${PORT}`);
});

为了确保它正在工作,请使用以下命令运行应用程序,然后在浏览器中打开 https://:8080/rolldice?rolls=12

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

手动配置遥测

依赖项

安装 OpenTelemetry API 包

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

初始化 SDK

如果您遥测的是 Node.js 应用程序,请安装适用于 Node.js 的 OpenTelemetry SDK

npm install @opentelemetry/sdk-node

在您的应用程序的任何其他模块加载之前,您必须初始化 SDK。如果您未能初始化 SDK 或初始化太晚,任何从 API 获取 Tracer 或 Meter 的库都将获得一个 no-op 实现。

/*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 将使用 no-op 实现并无法生成数据。正如接下来解释的,修改 instrumentation.ts(或 instrumentation.js)文件以包含 Node 和浏览器中的所有 SDK 初始化代码。

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 应用程序捆绑在一起,才能在 Web 应用程序的其余部分中使用追踪。

这目前不会对您的应用程序产生任何影响:您需要创建 Span才能让您的应用程序发出遥测数据。

选择正确的 Span 处理器

默认情况下,Node SDK 使用 BatchSpanProcessor,Web SDK 示例也选择了此 Span 处理器。BatchSpanProcessor 在导出之前批量处理 Span。这通常是应用程序要使用的正确处理器。

相比之下,SimpleSpanProcessor 在 Span 创建时就进行处理。这意味着如果您创建 5 个 Span,每个 Span 都将在代码中创建下一个 Span 之前被处理和导出。这在不希望丢失批次的情况下可能很有用,或者如果您正在开发中尝试 OpenTelemetry。但是,它也可能带来显着的开销,特别是当 Span 通过网络导出时 - 每次调用创建 Span 时,它都会在应用程序执行继续之前被处理并通过网络发送。

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

获取 Tracer

在应用程序中任何编写手动追踪代码的地方都应该调用 getTracer 来获取一个 Tracer。例如

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

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

// You can now use a 'tracer' to do tracing!
const opentelemetry = require('@opentelemetry/api');
//...

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

// You can now use a 'tracer' to do tracing!

instrumentation-scope-nameinstrumentation-scope-version 的值应唯一标识Instrumentation Scope,例如包、模块或类名。虽然名称是必需的,但版本尽管是可选的,但仍然推荐。

通常建议在应用程序中需要时调用 getTracer,而不是将 tracer 实例导出到应用程序的其他部分。这有助于避免在涉及其他必需依赖项时出现更棘手的应用程序加载问题。

示例应用程序的情况下,有两个地方可以用适当的 Instrumentation Scope 获取 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 https://:${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 https://:${PORT}`);
});

其次,在*库文件* dice.ts(或 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

现在您已经初始化了Tracer,您可以创建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) {
  // Create a span. A span must be closed.
  return tracer.startActiveSpan('rollTheDice', (span: Span) => {
    const result: number[] = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(min, max));
    }
    // Be sure to end the span!
    span.end();
    return result;
  });
}
function rollTheDice(rolls, min, max) {
  // Create a span. A span must be closed.
  return tracer.startActiveSpan('rollTheDice', (span) => {
    const result = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(min, max));
    }
    // Be sure to end the span!
    span.end();
    return result;
  });
}

如果您已按照示例应用程序的说明进行到这一点,您可以将上面的代码复制到您的库文件 dice.ts(或 dice.js)中。您现在应该能够看到从您的应用程序发出的 Span。

如下启动您的应用程序,然后通过访问 https://: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

嵌套 spans 让您可以追踪本质上是嵌套的工作。例如,下面的 rollOnce() 函数代表一个嵌套操作。下面的示例创建了一个追踪 rollOnce() 的嵌套 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.end();
    return result;
  });
}

export function rollTheDice(rolls: number, min: number, max: number) {
  // Create a span. A span must be closed.
  return tracer.startActiveSpan('rollTheDice', (parentSpan: Span) => {
    const result: number[] = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(i, min, max));
    }
    // Be sure to end the 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) {
  // Create a span. A span must be closed.
  return tracer.startActiveSpan('rollTheDice', (parentSpan) => {
    const result = [];
    for (let i = 0; i < rolls; i++) {
      result.push(rollOnce(i, min, max));
    }
    // Be sure to end the span!
    parentSpan.end();
    return result;
  });
}

此代码为每次*掷骰子*创建一个子 Span,其父 ID 为 parentSpan 的 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');
  // do some work
  const span2 = tracer.startSpan('work-2');
  // do some more work
  const span3 = tracer.startSpan('work-3');
  // do even more work

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

在这个例子中,span1span2span3 是同级 Span,它们都不是当前活动的 Span。它们共享同一个父项,而不是相互嵌套。

这种安排在您拥有分组在一起但概念上相互独立的工作单元时可能很有用。

获取当前 Span

有时,在程序的特定执行点处理当前/活动Span会很有帮助。

const activeSpan = opentelemetry.trace.getActiveSpan();

// do something with the active span, optionally ending it if that is appropriate for your use case.

从上下文中获取 Span

从给定的上下文中获取Span也可能很有帮助,而该上下文不一定是活动 Span。

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

// do something with the acquired span, optionally ending it if that is appropriate for your use case.

Attributes

属性允许您将键值对附加到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);

    // Add an attribute to the 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);

    // Add an attribute to the span
    span.setAttribute('dicelib.rolled', result.toString());

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

您也可以在创建 Span 时向其添加属性

tracer.startActiveSpan(
  'app.new-span',
  { attributes: { attribute1: 'value1' } },
  (span) => {
    // do some work...

    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 的语义约定定义在Trace Semantic Conventions规范中。在本指南的简单示例中,可以使用源代码属性。

首先,将语义约定添加为应用程序的依赖项

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');

最后,您可以更新您的文件以包含语义属性

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

    // Do some work...

    span.end();
  });
};

Span 事件

一个Span EventSpan上的一个人类可读的消息,它表示一个没有持续时间的离散事件,并且可以由单个时间戳跟踪。您可以将其视为原始日志。

span.addEvent('Doing something');

const result = doWork();

您还可以创建带有额外属性的 Span 事件

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

Spans 可以创建为零个或多个Links,这些链接指向其他在因果关系上相关的 Span。一种常见场景是将一个或多个追踪与当前 Span 相关联。

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

  tracer.startActiveSpan('app.someFunction', options, (span) => {
    // Do some work...

    span.end();
  });
};

Span Status

可以在 Span 上设置 Status,通常用于指定 Span 未成功完成 - Error。默认情况下,所有 spans 的状态都是 Unset,这意味着 span 完成时没有错误。Ok 状态保留给您需要明确将 span 标记为成功而不是坚持默认的 Unset(即,“无错误”)的情况。

可以在 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 设置为当前上下文中的活动 Span,才能创建嵌套 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({
    // Configure span processor to send spans to the exporter
    spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
  }),
);

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

// This is what we'll access in all instrumentation code
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({
    // Configure span processor to send spans to the exporter
    spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],
  }),
);

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

// This is what we'll access in all instrumentation code
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');

与本文档中的其他示例一样,这会导出一个您可以在应用程序中使用的 Tracer。

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

要创建嵌套 Span,您需要将当前创建的 Span 设置为当前上下文中的活动 Span。不要使用 startActiveSpan,因为它不会为您执行此操作。

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

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

  // Be sure to end the parent span!
  parentSpan.end();
};

const doWork = (parent, i) => {
  // To create a child span, we need to mark the current (parent) span as the active span
  // in the context, then use the resulting context to create a child span.
  const ctx = opentelemetry.trace.setSpan(
    opentelemetry.context.active(),
    parent,
  );
  const span = tracer.startSpan(`doWork:${i}`, undefined, ctx);

  // simulate some random work.
  for (let i = 0; i <= Math.floor(Math.random() * 40000000); i += 1) {
    // empty
  }

  // Make sure to end this child span! If you don't,
  // it will continue to track work beyond 'doWork'!
  span.end();
};

与 Node.js 或 Web SDK 相比,在使用 sdk-trace-base 时,所有其他 API 的行为都相同。

指标

指标将单个测量值合并成聚合,并生成随系统负载恒定的数据。聚合缺乏诊断低级问题所需的细节,但它们通过帮助识别趋势和提供应用程序运行时遥测来补充 Span。

初始化 Metrics

要启用您应用程序中的指标,您需要一个已初始化的MeterProvider,它允许您创建一个Meter

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

Node.js

如果您按照初始化 SDK的说明进行操作,那么您已经为您设置了一个 MeterProvider。您可以继续获取 Meter

使用 sdk-metrics 初始化指标

在某些情况下,您可能无法或不想使用完整的 OpenTelemetry Node.js 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(),
  // Default is 60000ms (60 seconds). Set to 10 seconds for demonstrative purposes only.
  exportIntervalMillis: 10000,
});

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

// Set this MeterProvider to be global to the app being instrumented.
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(),

  // Default is 60000ms (60 seconds). Set to 10 seconds for demonstrative purposes only.
  exportIntervalMillis: 10000,
});

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

// Set this MeterProvider to be global to the app being instrumented.
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);

您需要在运行应用程序时使用 --import 此文件,例如

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

现在已经配置了 MeterProvider,您可以获取 Meter

获取 Meter

在您的应用程序中任何手动遥测代码的地方,您都可以调用 getMeter 来获取一个 Meter。例如

import opentelemetry from '@opentelemetry/api';

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

// You can now use a 'meter' to create instruments!
const opentelemetry = require('@opentelemetry/api');

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

// You can now use a 'meter' to create instruments!

instrumentation-scope-nameinstrumentation-scope-version 的值应唯一标识Instrumentation Scope,例如包、模块或类名。虽然名称是必需的,但版本尽管是可选的,但仍然推荐。

通常建议在应用程序中需要时调用 getMeter,而不是将 meter 实例导出到应用程序的其他部分。这有助于避免在涉及其他必需依赖项时出现更棘手的应用程序加载问题。

示例应用程序的情况下,有两个地方可以用适当的 Instrumentation Scope 获取 Meter

首先,在*应用程序文件* 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 https://:${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 https://:${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 };

现在您已经初始化了Meter,您可以创建指标 Instrument

使用计数器

Counter 可用于测量非负的、递增的值。

在我们的示例应用程序的情况下,我们可以使用它来计算掷骰子次数

/*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);
}

使用 UpDown Counter

UpDown Counter 可以增减,允许您观察一个向上或向下变化的累积值。

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

//...

counter.add(1);

//...

counter.add(-1);

使用 Histogram

Histogram 用于测量随时间推移的值的分布。

例如,这是使用 Express 报告 Express 路由响应时间分布的方法

import express from 'express';

const app = express();

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

  // do some work in an API call

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

  // Record the duration of the task operation
  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();

  // do some work in an API call

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

  // Record the duration of the task operation
  histogram.record(executionTime);
});

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

Observable Counter 可用于测量一个累加的、非负的、单调递增的值。

const events = [];

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

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

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

//... calls to addEvent

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

Observable UpDown Counter 可以增减,允许您测量一个累加的、非负的、非单调递增的累积值。

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);
});

//... calls to addEvent and removeEvent

使用可观察(异步)仪表

Observable Gauge 应用于测量非累加值。

let temperature = 32;

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

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

//... temperature variable is modified by a sensor

描述 Instrument

当您创建计数器、直方图等 Instrument 时,可以为它们提供描述。

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

在 JavaScript 中,每种配置类型意味着以下内容

  • description - Instrument 的可读描述
  • unit - 用于表示值的测量单位的描述。例如,milliseconds 用于测量持续时间,或 bytes 用于计算字节数。
  • valueType - 在测量中使用的数值类型。

通常建议描述您创建的每个 Instrument。

添加属性

您可以在生成指标时为其添加属性。

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

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

配置指标视图

指标视图为开发人员提供了自定义 Metrics SDK 公开的指标的能力。

选择器

要实例化视图,首先必须选择一个目标 Instrument。以下是指标的有效选择器

  • instrumentType
  • instrumentName
  • meterName
  • meterVersion
  • meterSchemaUrl

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

示例

过滤所有指标类型的属性

const limitAttributesView = {
  // only export the attribute 'environment'
  attributeKeys: ['environment'],
  // apply the view to all instruments
  instrumentName: '*',
};

删除 meter 名称为 pubsub 的所有 Instrument

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

为名为 http.server.duration 的 Histogram 定义显式桶大小

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,
};

附加到 Meter Provider

配置完视图后,将它们附加到相应的 Meter Provider

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

日志

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

下一步

您还需要配置适当的导出器来导出您的遥测数据到一个或多个遥测后端。