分散トレーシングと APM

OxPHP は W3C Trace Context の伝播、OpenTelemetry (OTel) エクスポート、そして組み込みのアプリケーションパフォーマンスモニタリング (APM) をサポートしています。受信した traceparent ヘッダーは解析され、そのまま引き継がれます。トレース ID は PHP から $_SERVER 経由で参照でき、アクセスログにはトレースフィールドが含まれ、スパンは Jaeger、Grafana Tempo、Zipkin、または任意の OTLP 互換バックエンドにエクスポートできます。

APM プラグインは、OTel の基盤の上に 3 層のトレーシングを追加します。

  • 自動計装 — 内部 PHP 関数 (PDO、mysqli、cURL、Redis、Memcached、ファイル I/O) がエンジンレベルでフックされます。コードを一切変更することなく、すべての呼び出しがスパンになります
  • 属性ベースのトレーシング — 任意の PHP 関数やメソッドを #[OxPHP\Apm\Trace] でアノテートすると、スパンが自動的に生成されます
  • PHP SDK — 手動でのスパン生成、属性、イベント、エラー記録のための 10 個の oxphp_apm_*() 関数

仕組み

  1. リクエストの受信 — OxPHP は W3C Trace Context 仕様に従って traceparent および tracestate ヘッダーを読み取ります
  2. 新しいスパン — このホップ用に新しいスパン ID が生成されます。受信したスパン ID は親になります
  3. PHP への伝播 — トレース ID が $_SERVER['OXPHP_TRACE_ID']$_SERVER['OXPHP_SPAN_ID']$_SERVER['OXPHP_PARENT_SPAN_ID'] に注入されます
  4. アクセスログ — 構造化された JSON ログには、ログの関連付けのための trace_id および span_id フィールドが含まれます
  5. レスポンスヘッダー — 更新された traceparent ヘッダー (OxPHP のスパン ID を含む) がレスポンスに追加され、下流のサービスがトレースを継続できるようになります
  6. OTel エクスポート (任意) — OTel プラグインが有効な場合、各リクエストは HTTP セマンティック規約の属性を持つスパンとなり、OTLP 経由でエクスポートされます

traceparent ヘッダーが存在しない場合、OxPHP は新しいトレース ID とスパン ID を生成し、新規のトレースを開始します。

設定

W3C Trace Context (組み込み)

変数 デフォルト 説明
TRACE_CONTEXT false W3C Trace Context の伝播を有効にします。true または 1 を設定します

OpenTelemetry プラグイン

OTel プラグインはコンパイル時の機能 (plugin-otel) です。有効にすると、トレースコンテキストの伝播が自動的に有効になります (TRACE_CONTEXT=true を設定した場合と同じ効果です)。

変数 デフォルト 説明
OTEL_ENABLED false OpenTelemetry プラグインを有効にします。ブール値 — ブール値 を参照してください
OTEL_EXPORTER_OTLP_PROTOCOL grpc エクスポートプロトコル: grpc または http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 (gRPC) または http://localhost:4318 (HTTP) OTLP コレクターのエンドポイント。https:// URL は両方のトランスポートで TLS 経由でエクスポートされ、システムのトラストストアに対して検証されます (ランタイムイメージには ca-certificates のような CA バンドルが含まれている必要があります — 公式イメージにはインストール済みです)。カスタム CA バンドルおよび mTLS はまだサポートされていません
OTEL_EXPORTER_OTLP_TIMEOUT 10000 エクスポートのタイムアウト (ミリ秒)
OTEL_EXPORTER_OTLP_HEADERS (未設定) 認証ヘッダー: key=value,key2=value2
OTEL_SERVICE_NAME oxphp エクスポートされるスパンでのサービス名
OTEL_SERVICE_VERSION (未設定) サービスバージョンの属性
OTEL_RESOURCE_ATTRIBUTES (未設定) 追加のリソース属性: env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio サンプリング戦略: always_onalways_offtraceidratioparentbased_always_onparentbased_always_offparentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 比率ベースのサンプラーにおけるサンプリング比率 (0.0〜1.0)
Note

無効または範囲外の OTEL_TRACES_SAMPLER_ARG の値は [0.0, 1.0] にクランプされ、warn レベルでログに記録されます。認識されない OTEL_TRACES_SAMPLER の値は parentbased_traceidratio にフォールバックし、ログに記録されます。

APM プラグイン

APM プラグインはコンパイル時の機能 (plugin-apm) であり、OTel プラグインに依存します。自動計装、#[OxPHP\Apm\Trace] デコレーター、そして PHP トレーシング SDK を追加します。

変数 デフォルト 説明
OTEL_APM_ENABLED false APM を有効にします: 自動計装、エラーキャプチャ、PHP SDK。OTEL_ENABLED=true が必要です。ブール値 — ブール値 を参照してください
OTEL_APM_SLOW_QUERY_MS 100 スロークエリのしきい値 (ミリ秒)。これを超えるクエリはスパンに oxphp.db.slow=true が付与されます
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false バインドパラメータを db.params スパン属性に記録します。ブール値 — ブール値 を参照してください
OTEL_APM_STACKTRACE_MAX_BYTES 8192 exception.stacktrace 属性の最大サイズ (バイト)。上限を超えると、スタックトレースは末尾から切り詰められ (ルートフレームは保持されます)、…(truncated) マーカーが付与されます。0 で切り詰めを無効にします
OTEL_APM_MESSAGE_MAX_BYTES 4096 exception.message 属性の最大サイズ (バイト。デフォルト値は New Relic の属性ごとの値の上限に合わせています)。上限を超えると、メッセージは末尾から切り詰められ、…(truncated) マーカーが付与されます。0 で切り詰めを無効にします

PHP でのトレースコンテキスト

TRACE_CONTEXT=true の場合、PHP スクリプト内で 3 つの $_SERVER 変数が利用できます。

変数 説明
OXPHP_TRACE_ID W3C トレース ID (32 桁の 16 進数) 4bf92f3577b34da6a3ce929d0e0e4736
OXPHP_SPAN_ID このリクエストにおける OxPHP のスパン ID (16 桁の 16 進数) 00f067aa0ba902b7
OXPHP_PARENT_SPAN_ID 受信した親スパン ID (16 桁の 16 進数。新規トレースの場合は空) a3ce929d0e0e4736

これらを使って、下流のサービスにトレースコンテキストを伝播できます。

php
<?php $traceId = $_SERVER['OXPHP_TRACE_ID'] ?? ''; $spanId = $_SERVER['OXPHP_SPAN_ID'] ?? ''; if ($traceId) { // Build a traceparent header for downstream calls $traceparent = "00-{$traceId}-{$spanId}-01"; $response = file_get_contents('https://api.example.com/data', false, stream_context_create([ 'http' => [ 'header' => "traceparent: {$traceparent}\r\n", ], ]) ); }

Guzzle を使う場合

php
<?php $traceId = $_SERVER['OXPHP_TRACE_ID'] ?? ''; $spanId = $_SERVER['OXPHP_SPAN_ID'] ?? ''; $client = new \GuzzleHttp\Client(); $response = $client->get('https://api.example.com/users', [ 'headers' => [ 'traceparent' => "00-{$traceId}-{$spanId}-01", ], ]);

アクセスログの関連付け

トレースコンテキストが有効な場合、構造化された JSON アクセスログには trace_id および span_id フィールドが含まれます。

json
{ "timestamp": "2026-03-23T10:15:30.123Z", "level": "INFO", "fields": { "request_id": "4bf92f3577b34da6-00f067aa", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7", "method": "GET", "path": "/api/users", "status": 200, "duration_us": 1523, "remote_ip": "10.0.0.1", "message": "request completed" } }

これにより、ログ集約システム (Loki、Elasticsearch、Splunk、CloudWatch) でトレース ID からログを検索し、分散トレースに含まれるすべてのログエントリを見つけることができます。

レスポンスヘッダー

OxPHP は、すべてのレスポンスに OxPHP 自身のスパン ID を含む traceparent ヘッダーを追加します。

http
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

受信リクエストに tracestate ヘッダーが含まれていた場合、それもレスポンスに転送されます。

OpenTelemetry 連携

OTel プラグインが有効な場合、各 HTTP リクエストはスパンとなり、OTLP 経由でトレーシングバックエンドにエクスポートされます。

スパン属性

エクスポートされるスパンには、標準的な HTTP セマンティック規約の属性が含まれます。

属性 説明
http.request.method HTTP メソッド (GET、POST など)
url.path リクエストパス
http.response.status_code レスポンスステータスコード
client.address クライアントの IP アドレス
server.address サーバーのリッスンアドレス
oxphp.request_id OxPHP のリクエスト ID
http.request.body.size リクエストボディのサイズ (バイト。0 でない場合)
http.response.body.size レスポンスボディのサイズ (バイト。0 でない場合)

5xx レスポンスはエラースパンとしてマークされます。

スパンイベント

子スパンは スパンイベント も保持します。これはタイムスタンプ付きのアノテーションで、OpenTelemetry イベントとしてエクスポートされ、Jaeger、Grafana Tempo、その他の OTLP バックエンドでネイティブに表示されます。各イベントの oxphp.event.kind 属性が、その種類を示します。

oxphp.event.kind 発生源 イベント属性
exception 例外をスローした #[OxPHP\Apm\Trace] 関数、または oxphp_apm_error() exception.typeexception.messageexception.stacktrace
custom oxphp_apm_event() ユーザー指定
mark プロファイラーの #[Mark] アノテーション ユーザー指定
slow プロファイラーの #[SlowThreshold] 超過 threshold_mselapsed_ms
memory_spike プロファイラーの #[MemoryThreshold] 超過 threshold_kbdelta_bytes

APM 計装によって生成されたイベントでは、oxphp.event.kind 属性がさらに sqlhttpalloc を持つ場合があります。

OTel でのリクエスト ID

OTel プラグインが有効な場合、リクエスト ID はトレースコンテキストから導出されます。トレース ID の先頭 16 文字とスパン ID の先頭 8 文字をダッシュで区切ったものです。これはログ、X-Request-ID レスポンスヘッダー、そして PHP の oxphp_request_id() に現れます。

APM: 自動計装

APM プラグインが有効な場合、OxPHP は 33 個の内部 PHP 関数をエンジンレベルで自動的にフックします。フックされた関数への各呼び出しは、現在のリクエストのルートスパンの下に子スパンを生成します。コードを一切変更する必要はありません。

フックされる関数

カテゴリ 関数
PDO PDO::__constructPDO::queryPDO::execPDO::preparePDOStatement::execute
mysqli mysqli::__constructmysqli::querymysqli::preparemysqli_stmt::execute
cURL curl_initcurl_setoptcurl_execcurl_multi_exec
Redis Redis::connectRedis::getRedis::setRedis::delRedis::mgetRedis::msetRedis::hgetRedis::hsetRedis::lpushRedis::rpush
Memcached Memcached::getMemcached::setMemcached::deleteMemcached::getMultiMemcached::setMulti
ファイル I/O fopenfreadfwritefile_get_contentsfile_put_contents

フックは、実際にロードされている拡張機能に対してのみインストールされます。ビルドに Redis 拡張機能が含まれていない場合、Redis のフックは通知なくスキップされます。

フックのインストール

フックのインストールは、PHP ZTS 下でのスレッド安全性のために 2 段階の設計を採用しています。

  1. フェーズ 1 (MINIT) — モジュール初期化時に、OxPHP はロードされている拡張機能に対して各ターゲット関数を検証し、元のハンドラーポインタを読み取り専用の承認済みリストに取り込みます
  2. フェーズ 2 (RINIT) — ワーカースレッドごとの最初のリクエスト時に、承認済みのフックがそのスレッドの関数テーブルにインストールされます

これにより、各 ZTS ワーカースレッドが一貫した関数テーブルの変更とスレッドローカルな状態を持つことが保証されます。

APM: 属性ベースのトレーシング

#[OxPHP\Apm\Trace] 属性は、デコレートされた関数やメソッドの周囲にスパンを自動的に生成します。自動計装フック (内部 C 関数を対象とする) とは異なり、これはユーザー定義の PHP コードに対して機能します。

php
<?php use OxPHP\Apm\Trace; #[Trace] function processOrder(int $orderId): void { // A span named "processOrder" is created on entry and closed on exit. // If an exception is thrown, the span is marked as error and an // "exception" span event records exception.type, exception.message // and exception.stacktrace. } class PaymentService { #[Trace] public function charge(float $amount): bool { // Span named "PaymentService::charge" return true; } }

#[Trace] 属性は関数とメソッドの両方を対象とします。登録の呼び出しは不要です — APM プラグインが初期化時にデコレーターを自動的に登録します。

デコレートされた関数が例外をスローすると、スパンのステータスがエラーに設定され、完全な OpenTelemetry セマンティック規約データとともに exception イベントが記録されます: exception.type (クラス)、exception.message (メッセージ)、exception.stacktrace (getTraceAsString() から得られるコールスタック) です。メッセージは OTEL_APM_MESSAGE_MAX_BYTES バイト (デフォルト 4096) に、スタックトレースは OTEL_APM_STACKTRACE_MAX_BYTES バイト (デフォルト 8192) に切り詰められます。0 でいずれの上限も無効にできます。フレーム内の引数のキャプチャは、PHP 自身の zend.exception_ignore_args 設定に従います。

APM: PHP トレーシング SDK

APM プラグインは、手動でのスパン管理のための 10 個の oxphp_apm_*() 関数を登録します。すべての関数は APM が無効な場合には安全に何もしない (no-op) ため、どのような環境でもコードを変更せずに動作します。

スパンの生成

php
<?php // Start a span and get its local ID $spanId = oxphp_apm_start('cache.warm', ['cache.size' => '1024']); // ... do work ... // Close the span oxphp_apm_end($spanId);

属性とイベントの追加

php
<?php $spanId = oxphp_apm_start('order.process'); // Add attributes to the current span (or a specific one) oxphp_apm_attribute('order.id', $orderId); oxphp_apm_attribute('order.total', $total, $spanId); // Record an event on the span oxphp_apm_event('payment.authorized', [ 'provider' => 'stripe', 'amount' => (string) $amount, ]); oxphp_apm_end($spanId);

エラーの記録

php
<?php $spanId = oxphp_apm_start('external.api'); try { $result = callExternalApi(); } catch (\Throwable $e) { // Mark the span as error oxphp_apm_error($e, $spanId); throw $e; } finally { oxphp_apm_end($spanId); }

トレースコンテキストの伝播

php
<?php // Get the current trace ID and span ID $traceId = oxphp_apm_trace_id(); $currentSpanId = oxphp_apm_span_id(); // Or get a ready-to-use traceparent header value $traceparent = oxphp_apm_header(); // "00-{trace_id}-{span_id}-01" // Propagate to downstream services $response = file_get_contents('https://api.example.com/data', false, stream_context_create([ 'http' => [ 'header' => "traceparent: {$traceparent}\r\n", ], ]) );

関数リファレンス

関数 戻り値 説明
oxphp_apm_trace(name, callback, ?attributes) void スパン内でコールバックを実行します (将来の利用のために予約されています)
oxphp_apm_start(name, ?attributes) int スパンを開き、そのローカル ID を返します。APM が無効な場合は 0
oxphp_apm_end(span_id) void 指定されたローカル ID のスパンを閉じます
oxphp_apm_attribute(key, value, ?span_id) void 現在の、または指定されたスパンに属性を設定します
oxphp_apm_event(name, ?attributes, ?span_id) void 現在の、または指定されたスパンにタイムスタンプ付きのイベントを記録します
oxphp_apm_error(exception, ?span_id) void 現在の、または指定されたスパンをエラーとしてマークし、exception イベントを記録します。Throwable オブジェクトは exception.typeexception.messageexception.stacktrace を提供します。文字列引数のみの場合は、汎用の exception.type である Error の下に exception.message として記録されます (これにより、種類でグループ化するバックエンドでもイベントが表示され続けます)
oxphp_apm_status(code, ?description, ?span_id) void スパンのステータスを設定します: 0 = Unset、1 = Ok、2 = Error
oxphp_apm_trace_id() string 現在のトレース ID (32 桁の 16 進数)。APM が無効な場合は空
oxphp_apm_span_id() string 現在のスパン ID (16 桁の 16 進数)。アクティブなスパンがない場合は空
oxphp_apm_header() string 現在のスパンコンテキストに対応する W3C traceparent ヘッダーの値

関数シグネチャの完全なリファレンスについては、PHP 関数 を参照してください。

Docker の例

すぐに実行できる compose.yaml のバリエーションです。

外部バックエンドなしで W3C トレース伝播を有効にします。

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "80:80" environment: - TRACE_CONTEXT=true - INTERNAL_ADDR=0.0.0.0:9090
Note

plugin-apm の Cargo フィーチャーはビルド時に有効化されている必要があります。公式の OxPHP イメージにはデフォルトで含まれています。

オブザーバビリティスタック

OxPHP は、連携して機能する 3 つのオブザーバビリティの柱を提供します。

機能 関連付け
メトリクス /metrics での Prometheus カウンターとヒストグラム パフォーマンスデータの集計
ロギング ACCESS_LOG による構造化 JSON アクセスログ リクエストごとの詳細。trace_id で検索可能
トレーシング W3C Trace Context + OTLP エクスポート エンドツーエンドの分散リクエストフロー

3 つすべてが同じ trace_idrequest_id を共有するため、Grafana ダッシュボードのアラートから Tempo のトレースへ、さらに単一のリクエストに対応する Loki のログ行へと掘り下げていくことができます。

トラブルシューティング

レスポンスにトレースヘッダーが現れない

TRACE_CONTEXT が有効になっていません。

対処法: TRACE_CONTEXT=true を設定するか、OTEL_ENABLED=true で OTel プラグインを有効にしてください (これにより自動的にトレースコンテキストが有効になります)。

$_SERVER のトレース変数が空になる

トレースコンテキストが無効になっているか、変数が OxPHP の外部でチェックされています。

確認事項: OXPHP_TRACE_IDOXPHP_SPAN_IDOXPHP_PARENT_SPAN_ID の各変数は、TRACE_CONTEXT=true かつリクエストが OxPHP によって処理される場合にのみ存在します。次のようにテストします:

php
<?php echo $_SERVER['OXPHP_TRACE_ID'] ?? 'trace context not enabled';
スパンが Jaeger/Tempo に現れない

確認事項: OTLP エンドポイントが OxPHP コンテナから到達可能であることを確認します:

bash
docker compose exec app curl -v http://jaeger:4317

確認事項: プラグインが有効になっていることを確認します:

bash
curl -s http://localhost:9090/config | jq '.plugins'

対処法: OTEL_ENABLED=true であること、そして OTEL_EXPORTER_OTLP_ENDPOINT が正しいコレクターのアドレスを指していることを確認してください。

本番環境でのサンプリング量が多い

すべてのスパンをエクスポートすると、トラフィック量が多い場合にコストがかかります。

対処法: サンプリング比率を下げます:

bash
OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of traces

親ベースのサンプリングでは、受信リクエストがサンプリング済みのトレースを保持している場合、比率に関わらず常にサンプリングされます。OxPHP で開始された新しいトレースは、設定された比率でサンプリングされます。OTEL_TRACES_SAMPLER が認識されない値に設定された場合、OxPHP は警告をログに記録し、parentbased_traceidratio にフォールバックします。

関連項目