手动仪表化

OpenTelemetry JavaScript的手动仪表化

Manual instrumentation is the act of adding observability code to an app yourself.

If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.

If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.

For more information about the OpenTelemetry API and SDK, see the specification.

准备示例应用

本页面使用了从快速入门中修改的示例应用程序,以帮助您了解手动仪表化。

您不必使用示例应用程序:如果您想要为自己的应用程序或库进行仪表化,请按照这里的说明将该过程调整为适应您自己的代码。

依赖关系

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

npm init -y

接下来,安装Express的依赖项。

npm install typescript \
  ts-node \
  @types/node \
  express \
  @types/express
npm install express

创建并启动HTTP服务器

为了突出在对一个独立的_app_进行仪表化和对一个独立的_library_进行仪表化之间的区别,将"掷骰子"功能拆分为一个_library文件",然后它将作为一个依赖项被_app文件导入。

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

/*dice.ts*/
function rollOnce(min: number, max: number) {
  return Math.floor(Math.random() * (max - min) + 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) + 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文件_(如果不使用TypeScript,则为app.js),并将以下代码添加到其中:

/*app.ts*/
import express, { Request, 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}`);
});

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

$ npx ts-node 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 SDK for Node.js

npm install @opentelemetry/sdk-node

在您的应用程序中加载任何其他模块之前,您必须初始化SDK。如果您未能初始化SDK或初始化得太迟,将为从API获取追踪器或度量的任何库提供空操作实现。

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

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'yourServiceName',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0',
  }),
  traceExporter: new ConsoleSpanExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
});

sdk.start();
/*instrumentation.js*/
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
const {
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const {
  SemanticResourceAttributes,
} = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'dice-server',
    [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
  }),
  traceExporter: new ConsoleSpanExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
});

sdk.start();

为了调试和本地开发目的,以下示例将遥测导出到控制台。在设置完手动仪表化后,您需要配置适当的导出器,将应用程序的遥测数据导出到一个或多个遥测后端。

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

设置资源属性的其他方法也是可行的。有关更多信息,请参见资源

要验证您的代码,请通过引入库来运行应用程序:

npx ts-node --require ./instrumentation.ts app.ts
node --require ./instrumentation.js app.js

这个基本的设置对您的应用程序没有任何影响。您需要添加代码来处理跟踪度量和/或日志

您可以注册仪表化库到OpenTelemetry SDK for Node.js,以生成您的依赖项的遥测数据。有关更多信息,请参见

追踪

初始化追踪

要在你的应用中启用追踪功能,你需要初始化一个TracerProvider,此提供程序让您可以创建一个Tracer

如果没有创建TracerProvider,OpenTelemetry 追踪API将使用无操作实现并无法生成数据。接下来将会解释如何修改instrumentation.js(或instrumentation.ts)文件以在 Node.js 和浏览器中包含所有的 SDK 初始化代码。

Node.js

如果你按照[初始化 SDK](#初始化 SDK)的说明,你已经为你设置好了一个TracerProvider。你可以继续[获取一个 tracer](#获取一个 tracer)。

浏览器

首先,确保你已经安装了正确的包:

npm install @opentelemetry/sdk-trace-web

接下来,更新instrumentation.js(或instrumentation.ts), 包含以下所有的 SDK 初始化代码:

import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';

const resource = Resource.default().merge(
  new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'service-name-here',
    [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
  }),
);

const provider = new WebTracerProvider({
  resource: resource,
});
const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);
provider.addSpanProcessor(processor);

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

const resource = Resource.default().merge(
  new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'service-name-here',
    [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
  }),
);

const provider = new WebTracerProvider({
  resource: resource,
});
const exporter = new ConsoleSpanExporter();
const processor = new BatchSpanProcessor(exporter);
provider.addSpanProcessor(processor);

provider.register();

您需要将此文件与您的 Web 应用程序捆绑在一起,以便能够在应用程序的其余部分使用追踪。

到目前为止,此操作不会对你的应用程序产生任何影响,您需要[创建 spans](#创建 spans)才能使应用程序发出遥测信号。

选择正确的 span 处理程序

默认情况下,Node SDK 使用BatchSpanProcessor,在 Web SDK 示例中也选择了此 span 处理程序。BatchSpanProcessor会在导出前按批次处理 spans。这通常是用于应用程序的正确处理程序。

相比之下,SimpleSpanProcessor会在创建时处理 spans。这意味着如果您创建了 5 个 spans,每个 span 在下一个 span 创建之前都会被处理和导出。在某些情况下非常有用,比如不想冒失丢失一批范围的风险或在开发中尝试 OpenTelemetry。然而,在按需创建 span 的情况下,这也可能导致显著的开销,特别是当 spans 被导出到网络 - 每次创建 span 的调用时,在 app 执行前会被处理和发送到网络。

在大多数情况下,相比于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 的值应该能够唯一标识仪表盘范围,即包、模块或类的名称。虽然名称是必需的,但是版本尽管是可选的,还是建议提供。

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

对于示例应用程序,有两个地方可以获取由适当的仪表盘范围来获取 tracer:

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

/*app.ts*/
import { trace } from '@opentelemetry/api';
import express, { 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}`);
});

然后在 库文件 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) + 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) + 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 };

创建 spans

现在,您已经初始化了tracers,您可以创建spans了。

OpenTelemetry JavaScript API 提供了两种方法来创建 spans:

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

在大多数情况下,您应该使用后者(tracer.startActiveSpan), 因为它会同时设置 span 及其上下文。

下面的代码演示了如何创建一个 active span。

import { trace, 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)。您现在应该能够看到来自您的应用程序发出的 spans。

通过以下方式启动您的应该程序,然后通过访问http://localhost:8080/rolldice?rolls=12在您的浏览器或者 curl 发送请求给它。

ts-node --require ./instrumentation.ts app.ts
node --require ./instrumentation.js app.js

等待一段时间后,您应该在ConsoleSpanExporter中看到打印输出的 spans,像这样:

{
  "traceId": "6cc927a05e7f573e63f806a2e9bb7da8",
  "parentId": undefined,
  "name": "rollTheDice",
  "id": "117d98e8add5dc80",
  "kind": 0,
  "timestamp": 1688386291908349,
  "duration": 501,
  "attributes": {},
  "status": { "code": 0 },
  "events": [],
  "links": []
}

创建嵌套 spans

嵌套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) + 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) + 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;
  });
}

此代码为每个 roll 创建了一个子 span,它们以 parentSpan 的 ID 作为它们的父 ID:

{
  "traceId": "ff1d39e648a3dc53ba710e1bf1b86e06",
  "parentId": "9214ff209e6a8267",
  "name": "rollOnce:4",
  "id": "7eccf70703e2bccd",
  "kind": 0,
  "timestamp": 1688387049511591,
  "duration": 22,
  "attributes": {},
  "status": { "code": 0 },
  "events": [],
  "links": []
}
{
  "traceId": "ff1d39e648a3dc53ba710e1bf1b86e06",
  "parentId": undefined,
  "name": "rollTheDice",
  "id": "9214ff209e6a8267",
  "kind": 0,
  "timestamp": 1688387049510303,
  "duration": 1314,
  "attributes": {},
  "status": { "code": 0 },
  "events": [],
  "links": []
}

创建独立的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

有时,在程序执行的特定点上,获取当前/活动的span可能很有帮助。

const activeSpan = opentelemetry.trace.getActiveSpan();

// 做一些与活动span相关的操作,如果适用的话,可以选择结束该span。

从上下文中获取span

还可以从给定的上下文中获取span,这个span不一定是活动的span。

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

// 做一些与获取到的span相关的操作,如果适用的话,可以选择结束该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) + 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) + min);

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

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

您也可以在创建span时添加属性:

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 { SemanticAttributes } from '@opentelemetry/semantic-conventions';
const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');

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

const doWork = () => {
  tracer.startActiveSpan('app.doWork', (span) => {
    span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'doWork');
    span.setAttribute(SemanticAttributes.CODE_FILEPATH, __filename);

    // 做一些工作...

    span.end();
  });
};

Span事件

Span事件Span上的一个人类可读的消息,代表一个不需要持续时间,可以通过单个时间戳跟踪的离散事件。您可以将其看作是一个原始日志。

span.addEvent('正在执行某些操作');

const result = doWork();

您还可以使用附加的属性创建Span事件:

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

Span链接

Span可以和零个或多个与之有因果关系的其他Span相关联。一个常见的情况是将一个或多个跟踪与当前span相关联。

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

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

    span.end();
  });
};

Span状态

可以在span上设置一个状态,通常用于指定一个span未成功完成 - SpanStatusCode.ERROR

状态可以在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的状态都是未设置(Unset),而不是OK。通常,您的遥测管道中的另一个组件会解释span的未设置状态,因此最好不要覆盖它,除非您明确跟踪错误。

记录异常

在发生异常时,记录异常可能是一个好主意。建议在设置span状态时进行记录。

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

// ...

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

// ...

try {
  doWork();
} catch (ex) {
  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 {
  BasicTracerProvider,
  BatchSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';

const provider = new BasicTracerProvider();

// 配置span processor以将span发送到导出器
provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter()));
provider.register();

// 这是您在所有仪表化代码中要访问的部分
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');
const opentelemetry = require('@opentelemetry/api');
const {
  BasicTracerProvider,
  ConsoleSpanExporter,
  BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');

const provider = new BasicTracerProvider();

// 配置span processor以将span发送到导出器
provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter()));
provider.register();

// 这是您在所有仪表化代码中要访问的部分
const tracer = opentelemetry.trace.getTracer('example-basic-tracer-node');

与此文档中的其他示例一样,这将导出一个跟踪器,您可以在整个应用程序中使用。

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

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

const doWork = (parent, i) => {
  // 要创建子span,我们需要将当前(父)span标记为上下文中的活动span,
  // 然后使用生成的上下文来创建一个子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();
};

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

##指标

要开始生成指标,您需要初始化一个MeterProvider,它允许您创建一个MeterMeter允许您创建Instrument,您可以使用它们创建不同类型的指标。OpenTelemetry JavaScript目前支持以下Instrument

  • 计数器(Counter):支持非负增量的同步仪表
  • 异步计数器(Asynchronous Counter):支持非负增量的异步仪表
  • 直方图(Histogram):支持具有统计意义的任意值的同步仪表,例如直方图、摘要或百分位数
  • 异步测量仪表(Asynchronous Gauge):支持非加性值的异步仪表,例如室温
  • 上下计数器(UpDownCounter):支持增量和减量的同步仪表,例如活动请求的数量
  • 异步上下计数器(Asynchronous UpDownCounter):支持增量和减量的异步仪表

有关同步和异步仪表以及哪种类型最适合您的用例的更多信息,请参见附加指南

如果没有由仪表库创建MeterProvider,也没有手动创建,OpenTelemetry指标API将使用无操作实现并无法生成数据。

初始化指标

要在应用中启用指标,您需要初始化一个MeterProvider,以便创建一个Meter

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

Node.js

如果您按照上面的初始化SDK说明进行操作,您已经为自己设置了MeterProvider。您可以继续使用获取Meter

使用 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 { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const resource = Resource.default().merge(
  new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'dice-server',
    [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
  }),
);

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

  // 默认为 60000ms(60秒)。仅为演示目的设置为 3 秒。
  exportIntervalMillis: 3000,
});

const myServiceMeterProvider = new MeterProvider({
  resource: resource,
});

myServiceMeterProvider.addMetricReader(metricReader);

// 将此MeterProvider设置为应用程序的全局设置。
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);
const opentelemetry = require('@opentelemetry/api');
const {
  MeterProvider,
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const {
  SemanticResourceAttributes,
} = require('@opentelemetry/semantic-conventions');

const resource = Resource.default().merge(
  new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'service-name-here',
    [SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
  }),
);

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

  // 默认为 60000ms(60秒)。仅为演示目的设置为 3 秒。
  exportIntervalMillis: 3000,
});

const myServiceMeterProvider = new MeterProvider({
  resource: resource,
});

myServiceMeterProvider.addMetricReader(metricReader);

// 将此MeterProvider设置为应用程序的全局设置。
opentelemetry.metrics.setGlobalMeterProvider(myServiceMeterProvider);

当运行应用程序时,您需要--require此文件,例如:

ts-node --require ./instrumentation.ts app.ts
node --require ./instrumentation.js app.js

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

获取Meter

在应用程序的任何位置,您手动进行代码仪表化,并调用 getMeter 来获取一个 Meter。例如:

import opentelemetry from '@opentelemetry/api';

const myMeter = opentelemetry.metrics.getMeter('my-service-meter');

// 您现在可以使用 'meter' 创建仪表了!
const opentelemetry = require('@opentelemetry/api');

const myMeter = opentelemetry.metrics.getMeter('my-service-meter');

// 您现在可以使用 'meter' 创建仪表了!

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

同步和异步仪表

OpenTelemetry 仪表可以是同步的或异步的(可观察的)。

同步仪表对它们被调用时进行测量。测量作为程序执行期间的另一次调用进行,就像任何其他函数调用一样。定期地,这些测量的聚合将被导出为一个配置好的导出器。由于测量与导出值之间是解耦的,一个导出周期可能包含零个或多个聚合测量。

另一方面,异步仪表会根据SDK的请求提供测量。当SDK进行导出时,会调用在创建仪表时提供的回调函数。此回调函数向SDK提供一个立即导出的测量。所有异步仪表上的测量都是每个导出周期执行一次。

异步仪表在以下几种情况下非常有用:

  • 当更新计数器的计算成本较高,并且您不希望当前执行线程等待测量时
  • 观测需要以与程序执行无关的频率发生(即,它们不能在绑定到请求生命周期时被准确测量)
  • 对于测量值没有已知的时间戳

在这些情况下,通常最好直接观察一个累计值,而不是在后处理过程中聚合一系列增量(同步示例)。请注意,在下面的适当代码示例中使用的是observe而不是add

使用计数器

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

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

//...

counter.add(1);

使用上下计数器

上下计数器可以进行递增和递减,允许您观察递增或递减的累计值。

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

//...

counter.add(1);

//...

counter.add(-1);

使用直方图

直方图用于随时间测量值的分布。

例如,这是如何报告表达的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);
});

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

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

let events = [];

const addEvent = (name) => {
  events = append(events, name);
};

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

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

//...调用addEvent

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

可观察上下计数器可以进行递增和递减,允许您测量非负、非单调递增的累积值。

let events = [];

const addEvent = (name) => {
  events = append(events, name);
};

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

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

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

//...调用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: 'HTTP服务器响应时间的分布',
    unit: '毫秒',
    valueType: ValueType.INT,
  },
);

在JavaScript中,每种配置类型的含义如下:

  • description - 仪表的人类可读描述
  • unit - 执行时间的单位的描述,例如,测量持续时间使用毫秒,或计数字节的数量使用字节
  • valueType - 测量中使用的数值类型

通常建议为您创建的每个仪表进行描述。

添加属性

在生成度量时,可以向度量添加属性。

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

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

配置度量视图

度量视图提供了自定义指标的能力。

选择器

要实例化一个视图,必须首先选择目标仪表。以下是度量的有效选择器:

  • instrumentType
  • instrumentName
  • meterName
  • meterVersion
  • meterSchemaUrl

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

示例

筛选所有度量类型的属性:

const limitAttributesView = new View({
  // 仅导出属性 'environment'
  attributeKeys: ['environment'],
  // 将视图应用于所有仪表
  instrumentName: '*',
});

删除所有仪表的名为 pubsub 的仪表:

const dropView = new View({
  aggregation: new DropAggregation(),
  meterName: 'pubsub',
});

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

const histogramView = new View({
  aggregation: new ExplicitBucketHistogramAggregation([
    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目前正在开发中。

##下一步

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

最后修改 December 13, 2023: improve glossary translation (46f8201b)