在本页中,你将学习如何手动向代码添加链路、指标和日志。
但是,你并不局限于只使用一种插桩方式:
可以使用自动插桩来入门,然后根据需要使用手动插桩来丰富代码。
示例应用程序准备 {#example-app}
本页使用了入门指南中的示例应用程序的修改版本,帮助你学习手动插桩。
你无需使用该示例应用程序:如果你希望为自己的应用程序或库实现插桩,可遵循此处的说明,将该流程调整适配到你自己的代码中。
依赖项 {#example-app-dependencies}
在一个新目录下创建一个空的 NPM package.json 文件:
npm init -y
然后,安装 Express 依赖项。
npm install express @types/express
npm install -D tsx # 一款可通过 Node.js 直接运行 TypeScript(.ts)文件的工具
npm install express
创建和启动一个 HTTP 服务器 {#create-and-launch-an-http-server}
为了突出类库和独立应用程序实现插桩的区别,
将掷骰子逻辑提取到一个类库文件中,该文件将作为应用程序文件的依赖项导入。
创建名为 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
手动插桩配置 {#manual-instrumentation-setup}
依赖项 {#dependencies}
安装 OpenTelemetry API 包:
npm install @opentelemetry/api @opentelemetry/resources @opentelemetry/semantic-conventions
初始化 SDK {#initialize-the-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({
}),
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({
}),
traceExporter: new ConsoleSpanExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
}),
});
sdk.start();
出于调试和本地开发目的,以下示例将遥测数据导出到控制台。
完成手动插桩的配置后,你需要配置一个合适的导出器,以将应用的遥测数据导出至一个或多个遥测后端。
该示例还设置了强制性的 SDK 默认属性 service.name,它保存服务的逻辑名称,
并设置了可选的(但强烈建议!)属性 service.version,它保存服务 API 或实现的版本。
设置资源属性还存在其他方法。如需了解更多信息,请参见资源。
以下使用 --import instrumentation.ts(TypeScript)的示例需要 Node.js v20 或更高版本。
如果你使用的是 Node.js v18,请使用 JavaScript 示例。
npx tsx --import ./instrumentation.ts app.ts
node --import ./instrumentation.mjs app.js
此基本设置尚未对应用程序产生任何影响。
你需要添加链路、指标、日志的代码。
你可以将类库注册到 OpenTelemetry Node.js 版 SDK 中,以便为你的依赖项生成遥测数据。
如需了解更多信息,请参见类库。
链路 {#traces}
初始化链路追踪 {#initialize-tracing}
如果你正在为一个类库实现插桩,请跳过此步骤。
要在应用程序中启用链路追踪,
你需要有一个已初始化的TracerProvider,
它将允许你创建一个 Tracer。
如果未创建 TracerProvider,则 OpenTelemetry 链路追踪 API 将使用无操作实现版本,无法生成数据。
如下文所述,请修改 instrumentation.ts(或 instrumentation.js)文件,
将所有 SDK 初始化代码纳入其中,以适配 Node 环境和浏览器环境。
Node.js {#nodejs}
如果你遵循了上述初始化 SDK 的说明,
你已经有一个为你设置好的 TracerProvider。
你可以继续获取一个 tracer。
浏览器 {#browser}
首先,确保你拥有正确的包:
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({
}),
);
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({
}),
);
const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);
const provider = new WebTracerProvider({
resource: resource,
spanProcessors: [processor],
});
provider.register();
你需要将此文件与你的 Web 应用程序捆绑在一起,来确保在应用程序启动时加载并执行此文件。
这尚未对你的应用程序产生任何影响:
你需要创建 Span,才能使你的应用程序生成遥测数据。
选择正确的 Span 处理器 {#picking-the-right-span-processor}
默认情况下,Node SDK 使用 BatchSpanProcessor,Web SDK 示例中也选择了此 Span 处理器。BatchSpanProcessor 会在导出之前批量处理 Span。
对于应用程序而言,这通常是适用的处理器。
相比之下,SimpleSpanProcessor 会在 Span 创建时立即处理 Span。
这意味着如果你创建了 5 个 Span,
每个 Span 都会立即被处理和导出,而不会等待其他 Span 的创建。
在你不想承担批次数据丢失风险的场景下,或是在开发环境中试用 OpenTelemetry 时,这一方式会十分实用。
但是,这也可能带来显著的开销,尤其是在通过网络导出 Span 的情况下——
每个 Span 创建时都需要进行网络调用,这会增加应用程序的延迟。
在大多数情况下,请坚持使用 BatchSpanProcessor,而非 SimpleSpanProcessor。
获取一个追踪器 {#acquiring-a-tracer}
在应用程序中的任何地方编写手动链路追踪代码时,都应调用 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-name 和 instrumentation-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 {#create-spans}
现在你已经有了已初始化的追踪器,
你可以使用它来创建 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 {#create-nested-spans}
嵌套 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 {#create-independent-spans}
前述示例演示了如何创建活跃 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();
};
在此示例中,span1、span2 和 span3 是同级 Span,
它们都不被视为当前活动 Span。
它们共用同一个父级,而非彼此嵌套。
若你有一些归为一组、但在逻辑层面彼此独立的工作单元,这种结构会十分实用。
获得当前 Span {#get-the-current-span}
在程序执行过程中的特定节点,
对当前或活跃 Span 执行某些操作,有时会起到事半功倍的效果。
const activeSpan = opentelemetry.trace.getActiveSpan();
// 对活跃 Span 执行某些操作,视情况可选是否结束它。
从上下文获取 Span {#get-a-span-from-context}
从给定的上下文中获取 Span 也会很有帮助,这个上下文未必是当前活跃的 Span。
const ctx = getContextFromSomewhere();
const span = opentelemetry.trace.getSpan(ctx);
// 对获取到的 Span 执行某些操作,视情况可选是否结束它。
属性 {#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);
// 向 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) => {
/* ...... */
},
);
}
语义属性 {#semantic-attributes}
对于代表 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-events}
一个 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 链接 {#span-links}
Span 可以零个或多个 Link 链接到其他因果相关的 Span。
一种常见场景是将一条或多条追踪链路,与当前 Span 建立关联关系。
const someFunction = (spanToLinkFrom) => {
const options = {
links: [
{
context: spanToLinkFrom.spanContext(),
},
],
};
tracer.startActiveSpan('app.someFunction', options, (span) => {
// 执行一些工作......
span.end();
});
};
Span 状态 {#span-status}
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();
});
记录异常 {#recording-exceptions}
当异常发生时,记录异常是一个好主意。建议将其 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 上下文 {#using-sdk-trace-base-and-manually-propagating-span-context}
在某些情况下,你可能无法使用 Node.js SDK 或 Web SDK。
除初始化代码外,最大的区别在于,你必须手动将 Span 设置为当前上下文中的活跃状态,才能创建嵌套跨度。
使用 `sdk-trace-base` 初始化追踪功能 {#initializing-tracing-with-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 {#creating-nested-spans-with-sdk-trace-base}
要创建嵌套跨度,你需要将当前创建的 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 相同。
指标 {#metrics}
指标会将单独的测量数据聚合为汇总数据,
并生成不受系统负载影响的稳定数据。
聚合值缺少诊断低级问题所需的详细信息,但通过帮助识别趋势和提供应用程序运行时遥测,
补充了跨度。
初始化指标工具 {#initialize-metrics}
若你正在为某个库做插桩适配,则可跳过此步骤。
要在应用程序中启用指标工具,
你需要初始化一个 MeterProvider,
该提供程序将允许你创建一个 Meter。
如果未创建 MeterProvider,OpenTelemetry 指标 API 将使用无操作实现,无法生成数据。
如接下来所述,修改 instrumentation.ts(或 instrumentation.js)文件,
将所有 SDK 初始化代码包含在 Node 和浏览器中。
Node.js {#initialize-metrics-nodejs}
如果你遵循了上述初始化 SDK 的说明,
则已为你设置了一个 MeterProvider。你可以继续获取指标工具。
使用 `sdk-metrics` 初始化指标工具 {#initialize-metrics-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({
}),
);
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({
}),
);
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。
获取计量器 {#acquiring-a-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-name 和 instrumentation-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 };
使用计数器 {#using-counters}
计数器可用于测量非负、递增的值。
以示例应用为例,我们可以使用它来统计骰子被掷的次数:
/*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);
}
使用增减计数器 {#using-updown-counters}
增减计数器支持递增和递减操作,可用于观测一个可增可减的累计值。
const counter = myMeter.createUpDownCounter('events.counter');
//......
counter.add(1);
//......
counter.add(-1);
使用直方图 {#using-histograms}
直方图用于测量一段时间内值的分布。
例如,以下示例展示了如何基于 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);
});
使用可观测(异步)计数器 {#using-observable-async-counters}
可观测计数器可用于计量具有累加性、非负性且单调递增的数值。
const events = [];
const addEvent = (name) => {
events.push(name);
};
const counter = myMeter.createObservableCounter('events.counter');
counter.addCallback((result) => {
result.observe(events.length);
});
//…… 对 addEvent 方法的调用
使用可观测(异步)增减计数器 {#using-observable-async-updown-counters}
可观测增减计数器支持递增和递减操作,可用于计量具备累加性、非负性但非单调递增的累计值。
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 方法的调用
使用可观测(异步)瞬时值计量器 {#using-observable-async-gauges}
可观测瞬时值计量器用于测量非累加性值。
let temperature = 32;
const gauge = myMeter.createObservableGauge('temperature.gauge');
gauge.addCallback((result) => {
result.observe(temperature);
});
//…… 温度变量由传感器修改
插桩说明 {#describing-instruments}
创建计数器、直方图等插桩时,你可以为它们添加描述信息。
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- 用于测量的数值类型。
通常建议描述你创建的每个插桩。
添加属性信息 {#adding-attributes}
你可在指标生成时为其添加属性信息。
const counter = myMeter.createCounter('my.counter');
counter.add(1, { 'some.optional.attribute': 'some value' });
配置指标视图 {#configuring-metric-views}
指标视图为开发者提供了自定义由指标 SDK 对外暴露的指标数据的能力。
选择器 {#selectors}
要实例化一个视图,需先选定目标指标插桩。以下是适用于指标的合法选择器:
instrumentTypeinstrumentNamemeterNamemeterVersionmeterSchemaUrl
选择器 instrumentName(类型为字符串)支持通配符,因此你可以使用 * 选择所有插桩,或使用 http* 选择所有名称以 http 开头的插桩。
示例 {#examples}
对所有指标类型进行属性筛选:
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,
};
关联至计量器提供器 {#attaching-to-meter-provider}
配置视图后,将它们关联到相应的计量器提供器:
const meterProvider = new MeterProvider({
views: [limitAttributesView, dropView, histogramView],
});
日志 {#logs}
日志 API 和 SDK 当前正在开发中。
下一步 {#next-steps}
你还需要配置一个合适的导出器,以将遥测数据导出至一个或多个遥测后端。