@opentelemetry/api編
概要
@opentelemetry/apiは下記の5つのAPIのInterfaceが定義されています。
trace: Span, Tracer, TracerProvider の管理
metrics: Meter, Counter, Histogram, Gauge の管理
context: 非同期処理を跨いだコンテキストの維持
propagation: HTTP ヘッダ等へのコンテキスト inject/extract
diag: OpenTelemetry 自体のデバッグログ
registerGlobalを通してこれらのAPIのProvider(TracerProvider, MeterProvider等)がグローバルに登録できるようになっています。
GLOBAL_OPENTELEMETRY_API_KEY=Symbol.for('opentelemetry.js.api.{major}') をキーに グローバル変数 で管理
複数の npm パッケージが同じ API インスタンスを共有可能
バージョン互換性チェック付き
それぞれのAPIオブジェクトはglobalThisとしてオブジェクトが管理されており、GLOBAL_OPENTELEMETRY_API_KEYをkeyとしてネストされてProviderが登録されています。
code:ts
type OTelGlobal = {
};
Symbol.forで npm 複数コピー間でも同じキーを参照できるようにすることで、どの環境でも同じOpenTelemetry APIインスタンスへアクセスできます。
code:md
globalThis (グローバルオブジェクト)
│
│
└── { version, trace, metrics, context, propagation, diag }
│
└── 各 API の Provider インスタンス
型レベルの工夫
Trace内部で使われるBaggageAPIのBaggageEntryMetadata(主に別サービスへTraceの属性を伝搬する時に使われるやつ)ではOpaque型を使うことで専用のバリデーション関数を通して作成されたことを型レベルで保証できるようになっています。
code:ts
/**
* Symbol used to make BaggageEntryMetadata an opaque type
*/
export const baggageEntryMetadataSymbol = Symbol('BaggageEntryMetadata');
// 直接作れない
const meta: BaggageEntryMetadata = { toString: () => "foo" };
// エラー: Property '__TYPE__' is missing
// 専用関数でのみ作成可能
const meta = baggageEntryMetadataFromString("foo");
安全にAPIを扱える仕組み
SDKが存在しない場合はNOOP_CONTEXT_MANAGERが登録されることで、実際のアプリケーションの動作に影響を与えないような仕組みになっています。
code:ts
private _getContextManager(): ContextManager {
return getGlobal(API_NAME) || NOOP_CONTEXT_MANAGER;
}
code:ts
// trace
const NOOP_TRACER = new NoopTracer();
// metrics
const NOOP_METER_PROVIDER = new NoopMeterProvider();
// propagation
const NOOP_TEXT_MAP_PROPAGATOR = new NoopTextMapPropagator();
TraceAPI等の内部ではProxyTracer / ProxyTracerProviderにより、SDK が後から登録されても、既に取得した Tracer が動作する仕組みになっています。SDK未登録時はNoopTracerにフォールバックします。
code:md
1. アプリ起動時にtracer = trace.getTracer('my-app') を取得
→ ProxyTracerが返る(まだ SDK なし)
2. 後から SDK を登録: trace.setGlobalTracerProvider(provider)
→ ProxyTracerProviderにdelegateがセットされる
3. tracer.startSpan()を呼ぶ
→ ProxyTracerがdelegateから本物のTracerを取得して使用
Experimental
Experimentalの機能で例外処理やspanの終了処理が内包されたSugaredTracerというものがあります。
デフォルトでは下記のようなExceptionハンドリングの関数があり、例外時に呼び出されるようになっています。
code:ts
const defaultOnException = (e: Error, span: Span) => {
span.recordException(e);
span.setStatus({
code: SpanStatusCode.ERROR,
});
};
wrapTracerを使用して既存のtracerをwrapして使うことによって、利用者側は簡単にTracerを使えるようになります。(確かにユーザー側でwrapしていたので公式が提供してくれるのは助かる)
まとめ
@opentelemetry/apiをみました。基本的にこの後見ていくAPI軍のInterfaceや核となるGlobalRegisterの実装がメインでした。Symbolを使うことで、API の利用者が意図しない方法でコンテキストの値を操作することを防いでいたりとGlobalなオブジェクトを扱う上での工夫が見て取れました。