コンテキスト

計装されたアプリケーションにおけるコンテキスト API の仕組みを学びましょう。

OpenTelemetry はテレメトリーデータの保存と伝搬によって動作します。 たとえば、計装されたアプリケーションがリクエストを受信してスパンが開始されると、子スパンを作成するコンポーネントがそのスパンを利用できなければなりません。 このニーズに対応するため、OpenTelemetry はアクティブコンテキストにスパンを保存します。

PHP 実行コンテキスト

コンテキスト API は単一の PHP 実行コンテキスト内でグローバルに利用可能であり、現在の実行コンテキスト内には1つのアクティブコンテキストしか存在できません。

ストレージ

コンテキストは値(たとえば Span)を保存でき、保存された値の追跡には Storage を使用します。 デフォルトでは、汎用的な ContextStorage が使用されます。 OpenTelemetry for PHP は、fibers を用いた非同期処理や並行処理のような、一般的でないユースケース向けに他のコンテキストストレージもサポートしています。

コンテキストキー

値はキーバリューペアとしてコンテキストに保存されます。 コンテキストキーは、コンテキストから値を保存および取得するために使用されます。

キーは OpenTelemetry\Context\Context::createKey() を呼び出すことで作成できます。 たとえば以下のようにします。

use OpenTelemetry\Context\Context;

$key1 = Context::createKey('My first key');
$key2 = Context::createKey('My second key');

アクティブコンテキスト

アクティブコンテキストは、Context::getCurrent() によって返されるコンテキストです。 コンテキストオブジェクトには、テレメトリーコンポーネントが互いに通信するためのエントリーが含まれています。 たとえば、スパンが作成されるとそれをアクティベートでき、新しいアクティブコンテキストが作成されてスパンが保存されます。 その後、別のスパンが作成されると、アクティブコンテキストからスパンを取得して親スパンとして使用できます。 アクティブなコンテキストがない場合は、空のコンテキストオブジェクトであるルートコンテキストが返されます。

use OpenTelemetry\Context\Context;

// アクティブコンテキストを返す
// アクティブなコンテキストがない場合は、ルートコンテキストが返される
$context = Context::getCurrent();

コンテキスト値の設定と取得

値は $context->with($key, $value) メソッドを使用してコンテキストに保存されます。 コンテキストエントリーを設定すると、そのストレージに $value を含む新しいエントリーを持つ新しいコンテキストが作成されます。

コンテキストは不変です。 コンテキストエントリーを設定すると、そのストレージに新しいエントリーを持つ新しいコンテキストが作成されます。 $context->with($key, $value) で値を設定し、$context->get($key) で取得します。 たとえば以下のようにします。

use OpenTelemetry\Context\Context;

$key = Context::createKey('some key');

// 新しいエントリーを追加する
$ctx2 = Context::getCurrent()->with($key, 'context 2');

// ctx2 は新しいエントリーを含む
var_dump($ctx2->get($key)); // "context 2"

// アクティブコンテキストは変更されていない
var_dump(Context::getCurrent()->get($key)); // NULL

現在のコンテキストで値が見つからない場合は、キーが見つかるか、ルートコンテキストに到達するまで、各親が順にチェックされます。

コンテキストのアクティベート

コンテキストは $context->activate() を呼び出すことでアクティブにできます。

use OpenTelemetry\Context\Context;

$key = Context::createKey('my-key');
$ctx = Context::getCurrent();
$ctx2 = $ctx->with($key, 'context 2');
$ctx2->activate();
assert($ctx2 === Context::getCurrent());

スコープ

$context->activate() の戻り値は Scope です。 そのコンテキストを非アクティブにするには、スコープの detach() を呼び出す必要があります。 これにより、以前アクティブだったコンテキストが再びアクティブになります。

$scope->detach() の戻り値は整数です。 戻り値が 0 の場合、スコープが正常にデタッチされたことを意味します。 0 以外の値は、呼び出しが予期しないものだったことを意味します。 これは、スコープに関連付けられたコンテキストが以下のいずれかの状態だった場合に発生する可能性があります。

  • すでにデタッチされている
  • 現在の実行コンテキストの一部ではない
  • アクティブコンテキストではない

DebugScope

コンテキストとスコープに関する問題の特定を支援するために、DebugScope が用意されています。 アサーションが有効な PHP ランタイムでは、アクティベートされた ContextDebugScope でラップされます。 DebugScope はスコープがアクティベートされたタイミングを追跡し、スコープがデタッチされなかった場合にエラーを発生させるデストラクターを持っています。 エラー出力には、どのコードがコンテキストをアクティベートしたかのバックトレースが含まれます。

以下のコードはスコープがデタッチされていないことを警告するエラーを発生させ、スコープが作成された場所のバックトレースを表示します。

use OpenTelemetry\Context\Context;

$key = Context::createKey('my-key');
$scope = Context::getCurrent()->with($key, 'value')->activate();

// $scope をデタッチせずに終了

これは一部の状況、特にレガシーアプリケーションが exitdie を使用する場合に問題となることがあります。 その場合、アクティブなスパンは完了およびエクスポートされず、DebugScope がエラーを報告します。

DebugScope がエラーを報告する理由を理解し、そのリスクを受け入れる場合は、OTEL_PHP_DEBUG_SCOPES_DISABLED を truthy な値に設定することでこの機能を完全に無効にできます。

ネストされたコンテキスト

アクティブコンテキストの実行はネストできます。 これが、トレースがネストされたスパンを持てる仕組みです。

use OpenTelemetry\Context\Context;

$key = Context::createKey('my-key');

var_dump(Context::getCurrent()->get($key)); //NULL
$scope2 = Context::getCurrent()->with($key, 'context 2')->activate();
var_dump(Context::getCurrent()->get($key)); //'context 2'
$scope3 = Context::getCurrent()->with($key, 'context 3')->activate();
var_dump(Context::getCurrent()->get($key)); //'context 3'

$scope3->detach(); //context 2 がアクティブ
$scope2->detach(); //元のコンテキストがアクティブ
var_dump(Context::getCurrent()->get($key)); //NULL

非同期環境でのコンテキスト

Swoole や Fiber ベースの Revolt イベントループなど、非同期 PHP プログラミングでは、複数のアクティブコンテキストが存在できますが、実行コンテキストごとにアクティブなコンテキストは1つだけです。

Fiber ベースの実装では、Context はアクティブな Fiber に関連付けられ、PHP の Fiber 初期化ハンドラー、フォークハンドラー、破棄ハンドラーにフックすることで、適切にフォーク、切り替え、破棄されます。

その他の非同期実装では、正しく連携するためにカスタムコンテキストストレージが必要になる場合があります。 ストレージの実装についてはレジストリを確認してください。