伝搬

JS SDKのコンテキスト伝搬

With context propagation, Signals can be correlated with each other, regardless of where they are generated. Although not limited to tracing, context propagation allows traces to build causal information about a system across services that are arbitrarily distributed across process and network boundaries.

For the vast majority of use cases, libraries that natively support OpenTelemetry or instrumentation libraries will automatically propagate trace context across services for you. It is only in rare cases that you will need to propagate context manually.

To learn more, see Context propagation.

自動コンテキスト伝搬

@opentelemetry/instrumentation-http@opentelemetry/instrumentation-expressなどの計装ライブラリは、サービス間でのコンテキストの伝搬を自動的に行います。

Getting Startedガイドに従った場合、/rolldiceエンドポイントにクエリを送信するクライアントアプリケーションを作成できます。

まず、dice-clientという新しいフォルダを作成し、必要な依存関係をインストールします。

npm init -y
npm install typescript \
  ts-node \
  @types/node \
  undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node

# TypeScriptを初期化
npx tsc --init
npm init -y
npm install undici \
  @opentelemetry/instrumentation-undici \
  @opentelemetry/sdk-node

次に、client.ts(またはclient.js)という新しいファイルを以下の内容で作成します。

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('http://localhost:8080/rolldice').then((response) => {
  response.body.json().then((json: any) => console.log(json));
});
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
  SimpleSpanProcessor,
  ConsoleSpanExporter,
} = require('@opentelemetry/sdk-trace-node');
const {
  UndiciInstrumentation,
} = require('@opentelemetry/instrumentation-undici');

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

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

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

Getting Startedの計装されたapp.ts(またはapp.js)が一つのシェルで実行されていることを確認してください。

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

二つ目のシェルを開始し、client.ts(またはclient.js)を実行します。

npx ts-node client.ts
node client.js

両方のシェルはスパンの詳細をコンソールに出力するはずです。 クライアントの出力は以下のようになります。

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

traceId(cccd19c3a2d10e589f01bfe2dc896dc2)とID(6f64ce484217a7bf)をメモしてください。 両方はクライアントの出力でも見つけることができます。

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

クライアントとサーバーアプリケーションは接続されたスパンを正常に報告します。これらを今バックエンドに送信すると、視覚化でこの依存関係が表示されます。

手動コンテキスト伝搬

前のセクションで説明したように、コンテキストを自動的に伝搬できない場合があります。 サービス間の通信に使用するライブラリに対応する計装ライブラリが存在しない場合があります。 または、そのようなライブラリが存在していても満たせない要件がある場合があります。

コンテキストを手動で伝搬する必要がある場合は、context APIを使用できます。

汎用例

以下の汎用例では、トレースコンテキストを手動で伝搬する方法を示します。

まず、送信側のサービスで、現在のcontextを注入する必要があります。

// 送信側サービス
import { context, propagation, trace } from '@opentelemetry/api';

// トレース情報を保持する出力オブジェクトのインターフェースを定義
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// そのインターフェースに準拠する出力オブジェクトを作成
const output: Carrier = {};

// traceparentとtracestateをコンテキストから出力オブジェクトに
// シリアライズ
//
// この例ではアクティブなトレースコンテキストを使用していますが、
// シナリオに適したコンテキストを使用できます
propagation.inject(context.active(), output);

// 出力オブジェクトからtraceparentとtracestate値を抽出
const { traceparent, tracestate } = output;

// その後、traceparentとtracestateデータを
// サービス間でプロパゲートするために使用する
// メカニズムに渡すことができます
// 送信側サービス
const { context, propagation } = require('@opentelemetry/api');
const output = {};

// traceparentとtracestateをコンテキストから出力オブジェクトに
// シリアライズ
//
// この例ではアクティブなトレースコンテキストを使用していますが、
// シナリオに適したコンテキストを使用できます
propagation.inject(context.active(), output);

const { traceparent, tracestate } = output;
// その後、traceparentとtracestateデータを
// サービス間でプロパゲートするために使用する
// メカニズムに渡すことができます

受信側のサービスでは、contextを(たとえば、解析されたHTTPヘッダーから)抽出し、それらを現在のトレースコンテキストとして設定する必要があります。

// 受信側サービス
import {
  type Context,
  propagation,
  trace,
  Span,
  context,
} from '@opentelemetry/api';

// 'traceparent'と'tracestate'を含む入力オブジェクトのインターフェースを定義
interface Carrier {
  traceparent?: string;
  tracestate?: string;
}

// "input"が'traceparent'と'tracestate'キーを持つオブジェクトと仮定
const input: Carrier = {};

// 'traceparent'と'tracestate'データをコンテキストオブジェクトに抽出
//
// その後、このコンテキストをトレースのアクティブコンテキストとして
// 扱うことができます
let activeContext: Context = propagation.extract(context.active(), input);

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

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

// 作成されたスパンを逆シリアル化されたコンテキストでアクティブに設定
trace.setSpan(activeContext, span);
// 受信側サービス
import { context, propagation, trace } from '@opentelemetry/api';

// "input"が'traceparent'と'tracestate'キーを持つオブジェクトと仮定
const input = {};

// 'traceparent'と'tracestate'データをコンテキストオブジェクトに抽出
//
// その後、このコンテキストをトレースのアクティブコンテキストとして
// 扱うことができます
let activeContext = propagation.extract(context.active(), input);

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

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

// 作成されたスパンを逆シリアル化されたコンテキストでアクティブに設定
trace.setSpan(activeContext, span);

そこから、逆シリアル化されたアクティブコンテキストがある場合、他のサービスからの同じトレースの一部となるスパンを作成できます。

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

// サーバーに接続
const client = net.createConnection({ port: 8124 }, () => {
  // シリアライズされたオブジェクトをサーバーに送信
  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();
    // クライアントから受信したJSONオブジェクトを解析
    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();
    }
  });
});

// ポート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にのみ依存していたため、すべての呼び出しはno-op命令であり、クライアントとサーバーはOpenTelemetryが使用されていないかのように動作します。

OpenTelemetryを有効にし、実際のコンテキスト伝搬を確認するために、以下の内容でinstrumentation.jsという追加ファイルを作成します。

// instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
  ConsoleSpanExporter,
  SimpleSpanProcessor,
} = require('@opentelemetry/sdk-trace-node');

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

sdk.start();

このファイルを使用して、計装を有効にしてサーバーとクライアントの両方を実行します。

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

および

node -r ./instrumentation client.js

クライアントがサーバーにデータを送信して終了した後、両方のシェルのコンソール出力にスパンが表示されるはずです。

クライアントの出力は以下のようになります。

{
  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,
  // ...
}

手動例と同様に、スパンはtraceIdid/parentIdを使用して接続されています。

次のステップ

伝搬についてさらに学ぶには、Propagators API仕様をお読みください。