分散トレーシングと 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_*()関数
仕組み
- リクエストの受信 — OxPHP は W3C Trace Context 仕様に従って
traceparentおよびtracestateヘッダーを読み取ります - 新しいスパン — このホップ用に新しいスパン ID が生成されます。受信したスパン ID は親になります
- PHP への伝播 — トレース ID が
$_SERVER['OXPHP_TRACE_ID']、$_SERVER['OXPHP_SPAN_ID']、$_SERVER['OXPHP_PARENT_SPAN_ID']に注入されます - アクセスログ — 構造化された JSON ログには、ログの関連付けのための
trace_idおよびspan_idフィールドが含まれます - レスポンスヘッダー — 更新された
traceparentヘッダー (OxPHP のスパン ID を含む) がレスポンスに追加され、下流のサービスがトレースを継続できるようになります - 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_on、always_off、traceidratio、parentbased_always_on、parentbased_always_off、parentbased_traceidratio |
OTEL_TRACES_SAMPLER_ARG |
1.0 |
比率ベースのサンプラーにおけるサンプリング比率 (0.0〜1.0) |
無効または範囲外の 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
$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
$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 フィールドが含まれます。
{
"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 ヘッダーを追加します。
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.type、exception.message、exception.stacktrace |
custom |
oxphp_apm_event() |
ユーザー指定 |
mark |
プロファイラーの #[Mark] アノテーション |
ユーザー指定 |
slow |
プロファイラーの #[SlowThreshold] 超過 |
threshold_ms、elapsed_ms |
memory_spike |
プロファイラーの #[MemoryThreshold] 超過 |
threshold_kb、delta_bytes |
APM 計装によって生成されたイベントでは、oxphp.event.kind 属性がさらに sql、http、alloc を持つ場合があります。
OTel でのリクエスト ID
OTel プラグインが有効な場合、リクエスト ID はトレースコンテキストから導出されます。トレース ID の先頭 16 文字とスパン ID の先頭 8 文字をダッシュで区切ったものです。これはログ、X-Request-ID レスポンスヘッダー、そして PHP の oxphp_request_id() に現れます。
APM: 自動計装
APM プラグインが有効な場合、OxPHP は 33 個の内部 PHP 関数をエンジンレベルで自動的にフックします。フックされた関数への各呼び出しは、現在のリクエストのルートスパンの下に子スパンを生成します。コードを一切変更する必要はありません。
フックされる関数
| カテゴリ | 関数 |
|---|---|
| PDO | PDO::__construct、PDO::query、PDO::exec、PDO::prepare、PDOStatement::execute |
| mysqli | mysqli::__construct、mysqli::query、mysqli::prepare、mysqli_stmt::execute |
| cURL | curl_init、curl_setopt、curl_exec、curl_multi_exec |
| Redis | Redis::connect、Redis::get、Redis::set、Redis::del、Redis::mget、Redis::mset、Redis::hget、Redis::hset、Redis::lpush、Redis::rpush |
| Memcached | Memcached::get、Memcached::set、Memcached::delete、Memcached::getMulti、Memcached::setMulti |
| ファイル I/O | fopen、fread、fwrite、file_get_contents、file_put_contents |
フックは、実際にロードされている拡張機能に対してのみインストールされます。ビルドに Redis 拡張機能が含まれていない場合、Redis のフックは通知なくスキップされます。
フックのインストール
フックのインストールは、PHP ZTS 下でのスレッド安全性のために 2 段階の設計を採用しています。
- フェーズ 1 (MINIT) — モジュール初期化時に、OxPHP はロードされている拡張機能に対して各ターゲット関数を検証し、元のハンドラーポインタを読み取り専用の承認済みリストに取り込みます
- フェーズ 2 (RINIT) — ワーカースレッドごとの最初のリクエスト時に、承認済みのフックがそのスレッドの関数テーブルにインストールされます
これにより、各 ZTS ワーカースレッドが一貫した関数テーブルの変更とスレッドローカルな状態を持つことが保証されます。
APM: 属性ベースのトレーシング
#[OxPHP\Apm\Trace] 属性は、デコレートされた関数やメソッドの周囲にスパンを自動的に生成します。自動計装フック (内部 C 関数を対象とする) とは異なり、これはユーザー定義の 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
// 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
$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
$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
// 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.type、exception.message、exception.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 トレース伝播を有効にします。
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- TRACE_CONTEXT=true
- INTERNAL_ADDR=0.0.0.0:9090Jaeger をトレーシングバックエンドとした、完全なオブザーバビリティスタックです。
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- OTEL_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_SERVICE_NAME=my-app
- OTEL_SERVICE_VERSION=1.0.0
- OTEL_RESOURCE_ATTRIBUTES=env=production
- INTERNAL_ADDR=0.0.0.0:9090
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPCservices:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- OTEL_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317
- OTEL_SERVICE_NAME=my-app
tempo:
image: grafana/tempo:latest
ports:
- "4317:4317"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"データベースクエリ、HTTP 呼び出し、キャッシュ操作、ファイル I/O を自動計装した完全なオブザーバビリティです。
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- OTEL_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_SERVICE_NAME=my-app
- OTEL_APM_ENABLED=true
- OTEL_APM_SLOW_QUERY_MS=50
- INTERNAL_ADDR=0.0.0.0:9090
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
environment:
- COLLECTOR_OTLP_ENABLED=trueplugin-apm の Cargo フィーチャーはビルド時に有効化されている必要があります。公式の OxPHP イメージにはデフォルトで含まれています。
オブザーバビリティスタック
OxPHP は、連携して機能する 3 つのオブザーバビリティの柱を提供します。
| 柱 | 機能 | 関連付け |
|---|---|---|
| メトリクス | /metrics での Prometheus カウンターとヒストグラム |
パフォーマンスデータの集計 |
| ロギング | ACCESS_LOG による構造化 JSON アクセスログ |
リクエストごとの詳細。trace_id で検索可能 |
| トレーシング | W3C Trace Context + OTLP エクスポート | エンドツーエンドの分散リクエストフロー |
3 つすべてが同じ trace_id と request_id を共有するため、Grafana ダッシュボードのアラートから Tempo のトレースへ、さらに単一のリクエストに対応する Loki のログ行へと掘り下げていくことができます。
トラブルシューティング
レスポンスにトレースヘッダーが現れない
TRACE_CONTEXT が有効になっていません。
対処法: TRACE_CONTEXT=true を設定するか、OTEL_ENABLED=true で OTel プラグインを有効にしてください (これにより自動的にトレースコンテキストが有効になります)。
$_SERVER のトレース変数が空になる
トレースコンテキストが無効になっているか、変数が OxPHP の外部でチェックされています。
確認事項: OXPHP_TRACE_ID、OXPHP_SPAN_ID、OXPHP_PARENT_SPAN_ID の各変数は、TRACE_CONTEXT=true かつリクエストが OxPHP によって処理される場合にのみ存在します。次のようにテストします:
<?php
echo $_SERVER['OXPHP_TRACE_ID'] ?? 'trace context not enabled';スパンが Jaeger/Tempo に現れない
確認事項: OTLP エンドポイントが OxPHP コンテナから到達可能であることを確認します:
docker compose exec app curl -v http://jaeger:4317確認事項: プラグインが有効になっていることを確認します:
curl -s http://localhost:9090/config | jq '.plugins'対処法: OTEL_ENABLED=true であること、そして OTEL_EXPORTER_OTLP_ENDPOINT が正しいコレクターのアドレスを指していることを確認してください。
本番環境でのサンプリング量が多い
すべてのスパンをエクスポートすると、トラフィック量が多い場合にコストがかかります。
対処法: サンプリング比率を下げます:
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of traces親ベースのサンプリングでは、受信リクエストがサンプリング済みのトレースを保持している場合、比率に関わらず常にサンプリングされます。OxPHP で開始された新しいトレースは、設定された比率でサンプリングされます。OTEL_TRACES_SAMPLER が認識されない値に設定された場合、OxPHP は警告をログに記録し、parentbased_traceidratio にフォールバックします。