@opentelemetry/sdk-trace-node編
こんにちは、@sugar235711です。
この記事は「ひとりで気になるOSSのソースコード全部読んで何かする Advent Calendar 2025」12日目の記事です。
前日の記事はこちら:@opentelemetry/sdk-trace-web編
@opentelemetry/sdk-trace-nodeはNode.js環境向けのTracing SDKです。Webとの最大の違いはAsyncLocalStorageを使った非同期コンテキスト管理です。
NodeTracerProvider: Node.js向けTracerProvider
AsyncLocalStorageContextManager: Node.js標準APIによる非同期コンテキスト管理
EventEmitterへのコンテキストバインド
https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node
このSDKではデフォルトでAsyncLocalStorageContextManagerが使われています。これは’内部的にAsyncLocalStorageをwrapしたものであり、昨今のJSランライムであれば特に何も気にせずに非同期処理まで考慮した分散トレースが実装できるようになります。
https://github.com/open-telemetry/opentelemetry-js/blob/a36e8e91c5759bfd1e6ef58e5a3d158c24be55d8/packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts#L21
このパッケージにはAsyncHooksContextManagerというContext Managerも含まれていマスが、こちらの利用は推奨されていません。
The former should be preferred over the latter as its implementation is substantially simpler than the latter's, and according to Node.js docs,
> While you can create your own implementation [of AsyncLocalStorage] on top of [AsyncHook], AsyncLocalStorage should be preferred as it is a performant and memory safe implementation that involves significant optimizations that are non-obvious to implement.
実際に使うと下記のようにCallback内の関数からContextを取り出しSpanを構成することが可能になります。
code:ts
const contextManager = new AsyncLocalStorageContextManager();
context.setGlobalContextManager(contextManager);
const tracer = provider.getTracer('example');
const span = tracer.startSpan('my-operation');
const ctx = trace.setSpan(context.active(), span);
// context.with() が呼ばれると
context.with(ctx, async () => {
// ↓ 内部では
// asyncLocalStorage.run(ctx, callback)
// asyncLocalStorage.getStore() → ctx
// trace.getSpan(ctx) → span
await someAsyncOperation();
// AsyncLocalStorageが自動的にコンテキストを維持
});
このAsyncLocalStorageContextManagerはAbstractAsyncHooksContextManagerを継承しています。
https://github.com/open-telemetry/opentelemetry-js/blob/a36e8e91c5759bfd1e6ef58e5a3d158c24be55d8/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts#L38
AbstractAsyncHooksContextManagerでは主にEventEmitterのバインド機能が実装されています。
パッチされるメソッドは下記であり、
code:ts
'addListener' as const,
'on' as const,
'once' as const,
'prependListener' as const,
'prependOnceListener' as const,
バインド自体はPatchMapが使用されています。
PatchMapはイベント名からWeakMapへのマッピングを保持するオブジェクトです。WeakMapは元のListenerからパッチ済みListenerへの対応関係を保持します。Symbol('OtListeners')というキーを持たせることで、通常のプロパティアクセスがされないような工夫がされています。
code:ts
bind<T>(context: Context, target: T): T {
if (target instanceof EventEmitter) {
return this._bindEventEmitter(context, target);
}
if (typeof target === 'function') {
return this._bindFunction(context, target);
}
return target;
}
code:md
EventEmitter インスタンス
│
└── Symbol('OtListeners') ← _kOtListeners
│
└── PatchMap
├── 'data': WeakMap
│ ├── originalListener1 → patchedListener1
│ └── originalListener2 → patchedListener2
│
├── 'error': WeakMap
│ └── originalListener3 → patchedListener3
│
└── 'close': WeakMap
└── originalListener4 → patchedListener4
この機能によってNode.jsで使われるEventEmitter内に対してもコンテキストを維持できるようになっています。
まとめ
Node.jsでのコンテキスト処理に関してみていきました。