传播

JavaScript SDK 的上下文传播

通过上下文传播,信号可以相互关联,无论它们在何处生成。虽然不仅限于追踪,但上下文传播允许追踪在跨越任意分布式进程和网络边界的服务中构建系统的因果信息。

对于绝大多数用例,原生支持 OpenTelemetry 的库或仪器库会为您自动跨服务传播追踪上下文。只有在极少数情况下,您才需要手动传播上下文。

要了解更多信息,请参阅上下文传播

自动上下文传播

@opentelemetry/instrumentation-http@opentelemetry/instrumentation-express 这样的仪表化库 会为您自动传播跨服务的上下文。

如果您遵循了入门指南,您可以创建一个客户端应用程序来查询/rolldice端点。

首先创建一个名为dice-client的新文件夹并安装所需的依赖项

npm init -y
npm install undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node
npm install -D tsx  # a tool to run TypeScript (.ts) files directly with node
npm init -y
npm install undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node

接下来,创建一个名为client.ts(或client.js)的新文件,内容如下

/* client.ts */
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
  instrumentations: [new UndiciInstrumentation()],
});
sdk.start();

import { request } from 'undici';

request('https://:8080/rolldice').then((response) => {
  response.body.json().then((json: any) => console.log(json));
});
/* instrumentation.mjs */
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
  instrumentations: [new UndiciInstrumentation()],
});
sdk.start();

const { request } = require('undici');

request('https://:8080/rolldice').then((response) => {
  response.body.json().then((json) => console.log(json));
});

确保您已在某个终端中运行了入门指南中的app.ts(或app.js)的仪表化版本

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

启动第二个终端并运行client.ts(或client.js

npx tsx client.ts
node client.js

两个终端都应向控制台发出 span 详细信息。客户端输出类似于以下内容

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
  parentSpanContext: undefined,
  traceState: undefined,
  name: 'GET',
  id: '6f64ce484217a7bf',
  kind: 2,
  timestamp: 1718875320295000,
  duration: 19836.833,
  attributes: {
    'url.full': 'https://:8080/rolldice',
    // ...
  },
  status: { code: 0 },
  events: [],
  links: []
}

请注意traceIdcccd19c3a2d10e589f01bfe2dc896dc2)和ID6f64ce484217a7bf)。这两个值都可以在客户端输出中找到

{
  resource: {
    attributes: {
      // ...
  },
  traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
  parentSpanContext: {
    traceId: 'cccd19c3a2d10e589f01bfe2dc896dc2',
    spanId: '6f64ce484217a7bf',
    traceFlags: 1,
    isRemote: true
  },
  traceState: undefined,
  name: 'GET /rolldice',
  id: '027c5c8b916d29da',
  kind: 1,
  timestamp: 1718875320310000,
  duration: 3894.792,
  attributes: {
    'http.url': 'https://:8080/rolldice',
    // ...
  },
  status: { code: 0 },
  events: [],
  links: []
}

您的客户端和服务器应用程序成功报告了连接的 span。如果您现在将它们发送到后端,可视化将显示此依赖关系。

手动上下文传播

在某些情况下,无法自动传播上下文,如前一节所述。可能没有匹配您正在使用的库的仪表化库来让服务相互通信。或者,即使这些库存在,也可能无法满足您的要求。

当您必须手动传播上下文时,可以使用上下文 API

通用示例

下面的通用示例演示了如何手动传播跟踪上下文。

首先,在发送服务上,您需要注入当前的context

// Sending service
import { context, propagation, trace } from '@opentelemetry/api';

// Define an interface for the output object that will hold the trace information.
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// Create an output object that conforms to that interface.
const output: Carrier = {};

// Serialize the traceparent and tracestate from context into
// an output object.
//
// This example uses the active trace context, but you can
// use whatever context is appropriate to your scenario.
propagation.inject(context.active(), output);

// Extract the traceparent and tracestate values from the output object.
const { traceparent, tracestate } = output;

// You can then pass the traceparent and tracestate
// data to whatever mechanism you use to propagate
// across services.
// Sending service
const { context, propagation } = require('@opentelemetry/api');
const output = {};

// Serialize the traceparent and tracestate from context into
// an output object.
//
// This example uses the active trace context, but you can
// use whatever context is appropriate to your scenario.
propagation.inject(context.active(), output);

const { traceparent, tracestate } = output;
// You can then pass the traceparent and tracestate
// data to whatever mechanism you use to propagate
// across services.

在接收服务上,您需要提取context(例如,从解析的 HTTP 头部中提取),然后将其设置为当前的跟踪上下文。

// Receiving service
import {
  type Context,
  propagation,
  trace,
  Span,
  context,
} from '@opentelemetry/api';

// Define an interface for the input object that includes 'traceparent' & 'tracestate'.
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// Assume "input" is an object with 'traceparent' & 'tracestate' keys.
const input: Carrier = {};

// Extracts the 'traceparent' and 'tracestate' data into a context object.
//
// You can then treat this context as the active context for your
// traces.
let activeContext: Context = propagation.extract(context.active(), input);

let tracer = trace.getTracer('app-name');

let span: Span = tracer.startSpan(
  spanName,
  {
    attributes: {},
  },
  activeContext,
);

// Set the created span as active in the deserialized context.
trace.setSpan(activeContext, span);
// Receiving service
import { context, propagation, trace } from '@opentelemetry/api';

// Assume "input" is an object with 'traceparent' & 'tracestate' keys
const input = {};

// Extracts the 'traceparent' and 'tracestate' data into a context object.
//
// You can then treat this context as the active context for your
// traces.
let activeContext = propagation.extract(context.active(), input);

let tracer = trace.getTracer('app-name');

let span = tracer.startSpan(
  spanName,
  {
    attributes: {},
  },
  activeContext,
);

// Set the created span as active in the deserialized context.
trace.setSpan(activeContext, span);

从那里,当您拥有反序列化的活动上下文后,您可以创建属于来自另一个服务的同一跟踪的 span。

您还可以使用上下文 API 以其他方式修改或设置反序列化的上下文。

自定义协议示例

当您需要在服务之间手动传播上下文时,一个常见的用例是当您使用自定义协议进行通信时。下面的示例使用基本的基于文本的 TCP 协议将序列化对象从一个服务发送到另一个服务。

开始创建一个名为propagation-example的新文件夹,并按以下方式初始化其依赖项

npm init -y
npm install @opentelemetry/api @opentelemetry/sdk-node

接下来创建client.jsserver.js文件,内容如下

// client.js
const net = require('net');
const { context, propagation, trace } = require('@opentelemetry/api');

let tracer = trace.getTracer('client');

// Connect to the server
const client = net.createConnection({ port: 8124 }, () => {
  // Send the serialized object to the server
  let span = tracer.startActiveSpan('send', { kind: 1 }, (span) => {
    const output = {};
    propagation.inject(context.active(), output);
    const { traceparent, tracestate } = output;

    const objToSend = { key: 'value' };

    if (traceparent) {
      objToSend._meta = { traceparent, tracestate };
    }

    client.write(JSON.stringify(objToSend), () => {
      client.end();
      span.end();
    });
  });
});
// server.js
const net = require('net');
const { context, propagation, trace } = require('@opentelemetry/api');

let tracer = trace.getTracer('server');

const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    const message = data.toString();
    // Parse the JSON object received from the client
    try {
      const json = JSON.parse(message);
      let activeContext = context.active();
      if (json._meta) {
        activeContext = propagation.extract(context.active(), json._meta);
        delete json._meta;
      }
      span = tracer.startSpan('receive', { kind: 1 }, activeContext);
      trace.setSpan(activeContext, span);
      console.log('Parsed JSON:', json);
    } catch (e) {
      console.error('Error parsing JSON:', e.message);
    } finally {
      span.end();
    }
  });
});

// Listen on port 8124
server.listen(8124, () => {
  console.log('Server listening on port 8124');
});

启动第一个终端运行服务器

$ node server.js
Server listening on port 8124

然后在第二个终端运行客户端

node client.js

客户端应立即终止,服务器应输出以下内容

Parsed JSON: { key: 'value' }

由于到目前为止的示例仅依赖于 OpenTelemetry API,因此所有对它的调用都是无操作指令,客户端和服务器的行为就像没有使用 OpenTelemetry 一样。

要启用 OpenTelemetry 并看到上下文传播的效果,请创建一个额外的名为instrumentation.js的文件,内容如下

// instrumentation.mjs
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  ConsoleSpanExporter,
  SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-node';

const sdk = new NodeSDK({
  spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())],
});

sdk.start();

使用此文件运行服务器和客户端,并启用仪表化

$ node --import ./instrumentation.mjs server.js
Server listening on port 8124

node --import ./instrumentation.mjs client.js

在客户端将数据发送到服务器并终止后,您应该在两个终端的控制台输出中看到 span。

客户端的输出如下所示

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: '4b5367d540726a70afdbaf49240e6597',
  parentId: undefined,
  traceState: undefined,
  name: 'send',
  id: '92f125fa335505ec',
  kind: 1,
  timestamp: 1718879823424000,
  duration: 1054.583,
  // ...
}

服务器的输出如下所示

{
  resource: {
    attributes: {
      // ...
    }
  },
  traceId: '4b5367d540726a70afdbaf49240e6597',
  parentId: '92f125fa335505ec',
  traceState: undefined,
  name: 'receive',
  id: '53da0c5f03cb36e5',
  kind: 1,
  timestamp: 1718879823426000,
  duration: 959.541,
  // ...
}

手动示例类似,span 使用traceIdid/parentId进行连接。

下一步

有关传播的更多信息,请阅读 传播器 API 规范