OpenTelemetry を利用して PHP アプリケーションのテレメトリデータを計装する方法をまとめました。
本エントリのコードは下記で公開しています。 github.com
OpenTelemetry とは
OpenTelemetry は、サービスやアプリケーションのテレメトリーデータ(トレース、メトリクス、ログなど)を計装、生成、収集、送信するためのオブザーバビリティフレームワークです。ベンダーニュートラルな OSS であり、CNCF インキュベーティングプロジェクトの一つです。PHP 用 SDK も実装されており、これを利用してテレメトリデータが計装できます。
OpenTelemetry 自体には、テレメトリデータを集約して分析するツール(Jaeger, Prometheus, Loki 等)やサービス(AWS X-Ray, New Relic, Datadog 等)は含まれていません。これらのデータを分析するためには、別途分析ツールが必要です。
アプリケーションを OpenTelemetry で計装しておくことで、バックエンドの分析ツールを柔軟に選択できるメリットがあります。例えば、ローカル環境では Jaeger、本番環境では AWS X-Ray を利用するといったことが、PHP コードの変更なしに可能です。
用語
本エントリで利用している OpenTelemetry 関連の用語をまとめました。正しい定義についてはリンク先や OpenTelemetry のドキュメントを参照してください。
用語 | 内容 |
---|---|
テレメトリデータ | システムの状態を示すデータ。システム外から観測することで、状態を把握し、監視やチューニングに利用する。 主要なデータに「メトリクス」「トレース」「ログ」がある。 https://newrelic.com/jp/blog/best-practices/what-is-telemetry-data |
計装 | インストルメンテーション(instrumentation)。システムコンポーネントがテレメトリデータを生成、送信する仕組み(コード)を組み込んで測定すること。 アプリケーションに計装コードを組み込むマニュアル計装(手動計装)とアプリケーションコードの変更が不要なゼロコード計装(自動計装)がある。 https://opentelemetry.io/ja/docs/concepts/instrumentation/ https://ja.wikipedia.org/wiki/%E8%A8%88%E8%A3%85 |
シグナル | トレース、メトリクス、ログなどのテレメトリデータの総称。 https://opentelemetry.io/docs/concepts/signals/ |
メトリクス | テレメトリデータの一つ。実行時に取得されるサービスの測定値。カウンターやゲージ、ヒストグラムなどがある。 https://opentelemetry.io/docs/concepts/signals/metrics/ |
トレース | テレメトリデータの一つ。アプリケーションやコンポーネントを通過するリクエスト経路。 https://opentelemetry.io/docs/concepts/signals/traces/ |
ログ | テレメトリデータの一つ。 いわゆるログ。構造化されることが推奨されている。 https://opentelemetry.io/docs/concepts/signals/logs/ |
スパン | トレースの構成要素。タスクや操作の単位を示す。同じリクエスト内では同じトレース ID を持つ。スパンは親子関係を持つ。リクエストで最初に生成されるスパンは親スパンを持たないのでルートスパンと呼ばれる。 https://opentelemetry.io/docs/concepts/signals/traces/#spans |
エクスポータ | テレメトリデータを送信するコンポーネント。OpenTelemetry SDK や OpenTelemetry collector で利用される。OpenTelemetry 標準の OTLP 用の他、Prometheus、AWS X-Ray や Datadog などのエクスポータがある。 https://opentelemetry.io/docs/concepts/components/#exporters |
レシーバ | テレメトリデータを受信するコンポーネント。OpenTelemetry collector で利用される。 https://opentelemetry.io/docs/collector/configuration/#receivers |
プロセッサ | 受信したテレメトリデータをエクスポータに送信する前に修正または変換するコンポーネント。OpenTelemetry collector で利用される。 https://opentelemetry.io/docs/collector/configuration/#processors |
OpenTelemetry 公式ドキュメントや実装、関連サイトでは略語も利用されています。
略語 | 内容 | 参照 |
---|---|---|
OTel | OpenTelemetry の略 | https://opentelemetry.io/docs/concepts/glossary/#otel |
O11y | Observability の略 | https://en.wikipedia.org/wiki/Observability_(software) |
OTLP | OpenTelemetry Protocol の略 | https://opentelemetry.io/docs/concepts/glossary/#otlp |
用語の詳細な定義は下記の OpenTelemetry 用語集を参照してください。
PHP アプリケーションのマニュアル計装(手動計装)
PHP アプリケーションから OpenTelemetry SDK を利用してトレースを計装します。
構成
OTel Collector
PHP アプリケーションからテレメトリデータ(ここではトレース)を OpenTelemetry Collector(OTel Collector) に送信します。OTel Collector は、OpenTelemetry で提供されているツールで、アプリケーションなどからテレメトリデータを受信し、必要であればデータの加工などを行い、Jaeger のような分析ツールに送信します。
PHP アプリケーションから直接分析ツールに送信することも可能ですが、OTel Collector をリレーすることで PHP コードでは OTel Collector に送信する役割のみを担い、その後の送信先の指定や送信先に合わせたプロトコル変換、データの加工などは OTel Collector に委ねるという構成が可能になります。OTel Collector はバッファリングしたり、利用メモリの上限を設定するなどの制御が可能なので、アプリケーション影響を最低限にして安定的にテレメトリデータを送信するという観点でも有用です。
アプリケーションの主たる関心事はテレメトリデータの送信ではないので、不要な責務を持たないよう OTel Collector を利用するのが良いと考えています。
OpenTelemetry 公式で配布されている OTel Collector には以下の 3 種類があります。
- [OpenTelemetry Collector Core Distro(otelcol)](https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol)
- [OpenTelemetry Collector Contrib Distro(otelcol-contrib)](https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib)
- [OpenTelemetry Collector Kubernetes Distro(otelcol-k8s)](https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-k8s)
otelcol は最低限のコンポーネント(レシーバ、プロセッサ、エクスポータ)のみを含んだコア実装です。otelcol-contrib は otelcol をベースに多くのコンポーネントを同梱しています。これは動作検証には適していますが、本番環境で利用する場合は必要なコンポーネントをのみを含むことが推奨されています。独自の Collector は [OpenTelemetry Collector Builder](https://github.com/open-telemetry/opentelemetry-collector/tree/main/cmd/builder) で生成できます。
分析サービスベンダーからは、otelcol をベースに独自の拡張が含んだコレクタが公開されています。例えば、AWS からは [ADOT Collector](https://github.com/aws-observability/aws-otel-collector) が公開されているので、AWS X-Ray などに送信する場合はこちらの利用をすると良いでしょう。
Jaeger
OTel Collector で受信したテレメトリデータを Jaeger に送信します。Jaeger はトレースの集約し、Web ベースの分析 UI を提供する OSS です。Docker コンテナが配布されており、ローカル環境でも手軽に利用できます。
動作環境
ここでは Docker Compose を利用して環境を構築します。
compose.yaml は下記です。php、otel-collector、jaeger サービスを起動します。jaeger は Web UI 用に 16686/tcp をホストに開放しています。
services: php: build: ./docker/php volumes: - .:/app working_dir: /app environment: # OpenTelemetry SDK 設定 OTEL_PHP_AUTOLOAD_ENABLED: true OTEL_SERVICE_NAME: app OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318 otel-collector: image: otel/opentelemetry-collector volumes: - ./docker/otel-collector/otel-collector-config.yaml:/etc/otelcol/config.yaml jaeger: image: jaegertracing/all-in-one environment: COLLECTOR_OTLP_ENABLED: "true" # OTLP コレクタを有効 ports: - 16686:16686 # Web UI
php サービスの build で指定している ./docker/php/Dockerfile
は下記です。開発用の PHP コンテナに composer コマンドをインストールしています。
FROM shin1x1/php-dev:8.3-fpm-bookworm COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
otel-collector サービスで指定している OTel Collector の設定ファイルは下記です。ポイントは、レシーバで OTLP データを HTTP で受信している点と、エクスポータで jaeger に OTLP データを gRPC(ローカル環境なので TLS は無効) で送信している点です。
receivers: # レシーバ設定 otlp: protocols: http: endpoint: 0.0.0.0:4318 processors: # プロセッサ設定 batch: exporters: # エクスポータ設定 otlp/jaeger: endpoint: jaeger:4317 tls: insecure: true debug: verbosity: detailed extensions: health_check: service: extensions: [health_check] pipelines: traces: #トレーサパイプライン設定 receivers: [otlp] processors: [batch] exporters: [otlp/jaeger, debug]
docker compose コマンドで起動します。
$ docker compose up -d
ブラウザで Jaeger の Web UI である http://localhost:16686/ にアクセスすると、下記のようなページが表示されます。
必要なパッケージ
PHP 版 OpenTelemetry SDK とテレメトリデータを送信するために下記のパッケージをインストールします。
- open-telemetry/sdk: OpenTelemetry SDK
- open-telemetry/exporter-otlp: テレメトリデータを OTLP 形式で出力
- php-http/guzzle7-adapter: テレメトリデータを送信する HTTP クライアント
$ composer require open-telemetry/sdk open-telemetry/exporter-otlp php-http/guzzle7-adapter # php-http/discovery による自動検出は不要なので n + enter をタイプ Do you trust "php-http/discovery" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] n
PHP コード
トレースデータの生成と送信を行う PHP コードを実装します。OpenTelemetry SDK でトレースデータ(スパン)を生成するには下記の流れで処理を行います。
- トレーサプロバイダ生成
- トレーサプロバイダでトレーサを生成
- トレーサでスパンを生成
下記がコード例です。ここでは、ルートスパンと func1() 内で func1 スパンを送信します。func1() では 20% の確率で例外をスローするので、この場合はルートスパンにエラー情報が格納されます(この場合は、func1 スパンは送信されません)。
<?php declare(strict_types=1); use OpenTelemetry\API\Globals; use OpenTelemetry\API\Trace\StatusCode; use OpenTelemetry\API\Trace\TracerInterface; require __DIR__ . '/vendor/autoload.php'; // トレーサプロバイダからトレーサを生成 $tracer = Globals::tracerProvider()->getTracer('trace'); // トレーサでルートスパンを生成 $span = $tracer->spanBuilder('trace.php') ->startSpan(); // ルートスパンを有効にしてスパンを生成 $scope = $span->activate(); try { func1($tracer); } catch (Throwable $e) { // 例外キャッチ時はルートスパンにエラー情報をセット $span->recordException($e)->setStatus(StatusCode::STATUS_ERROR); throw $e; } finally { // 有効なスコープを終了 $scope->detach(); // ルートスパンを終了(送信) $span->end(); } function func1(TracerInterface $tracer): void { // 引数のトレーサで func1 スパンを生成 $span = $tracer->spanBuilder('func1')->startSpan(); // エラー時スパンを生成するため、20% で例外をスロー if (random_int(1, 5) === 1) { throw new Exception('error in func1'); } $result = random_int(1, 100); // スパンに実行結果をアトリビュートにセット $span->setAttribute('result', $result); // func1 スパンを終了(送信) $span->end(); } echo 'done';
設定
OpenTelemetry SDK ではテレメトリデータの送信先やプロトコルなどを環境変数や ini ファイルで指定できます。本エントリで指定する環境変数は下記です。ここでは compose.yaml で必要な環境変数を設定しています。
環境変数 | 内容 |
---|---|
OTEL_PHP_AUTOLOAD_ENABLED | https://opentelemetry.io/docs/languages/php/sdk/#autoloading オートローダ読み込み時に SDK のグローバル設定を自動で行う。 ここでは設定項目は環境変数でセットする想定なので true にしておく。 デフォルト: false 設定: true |
OTEL_SERVICE_NAME | https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_service_name リソースアトリビュートの service.name の値。サービス名を指定。 デフォルト: unknown_service 設定: app |
OTEL_TRACES_EXPORTER | https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_traces_exporter トレースデータのエクスポータ。 otlp, jaeger, zipkin, none から選択。 デフォルト: otlp 設定: otlp |
OTEL_EXPORTER_OTLP_PROTOCOL | https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_protocol テレメトリデータの送信プロトコル。grpc, http/protobuf, http/json から選択。 デフォルト: http/protobuf 設定: http/protobuf |
OTEL_EXPORTER_OTLP_ENDPOINT | https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint テレメトリデータの送信先ベース URL。 デフォルト: grpc: http://localhost:4317 HTTP: http://localhost:4318 設定: http://otel-collector:4318 |
設定項目については下記にまとまっています。
- 設定項目: https://opentelemetry.io/docs/languages/sdk-configuration/
- 各言語毎の対応状況: https://github.com/open-telemetry/opentelemetry-specification/blob/main/spec-compliance-matrix.md
- PHP 固有の設定: https://opentelemetry.io/docs/languages/php/sdk/#configuration
実行
PHP コードを実行して、トレースを Jaeger に送信してみましょう。main.php を 実行します。
$ php main.php
実行すると、PHP コードから送信されたトレースが otel-collector を経由して、jaeger に送信されます。ブラウザで jaeger にアクセスすると、下記のようにトレースデータ(ルートスパンと子スパン)が表示されます。
このように OpenTelemetry SDK を利用して、任意の箇所にスパンを計装ができます。SDK にはログやメトリクスを計装するコンポーネントも含まれているので、これらを利用することで同じリクエスト(同じトレースID)を持つテレメトリデータを串刺しで確認することができます。
PHP アプリケーションのゼロコード計装(自動計装)
OpenTelemetry では、PHP アプリケーションをゼロコード計装するためのパッケージがいくつか提供されています。例えば、Laravel や Symfony、Slim などのフレームワークや Guzzle や PDO など特定のコンポーネントの挙動を計装するパッケージがあります。
https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation
ここでは Laravel で実装した DB アクセスと外部 API への HTTP アクセスを行う簡単な API を題材のトレースをゼロコードで計装します。
https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/Laravel
ゼロコード計装を行うパッケージ名は open-telemetry/opentelemetry-auto-*
という形式になっているので、この名前で Packagist を検索すると探すことができます。
https://packagist.org/?query=open-telemetry%2Fopentelemetry-auto
必要な拡張とパッケージ
Laravel でゼロコード計装を行うためには、open-telemetry/opentelemetry-auto-laravel パッケージをインストールします。このパッケージは opentelemetry 拡張が必要となるので、先にこの拡張をインストールしておきます。
$ pecl install opentelemetry
opentelemetry 拡張は指定した関数(メソッド)の実行前後に関数フックを提供します。この機能を利用することで、関数実行に関するスパンを計装できます。
composer で必要なパッケージをインストールします。HTTP クライアントライブラリは Laravel の依存パッケージに含まれるので php-http/guzzle7-adapte は不要です。
$ composer require \ open-telemetry/sdk \ open-telemetry/exporter-otlp \ open-telemetry/opentelemetry-auto-laravel
設定
マニュアル計装と同じ値を設定します。docker compose であれば、下記のようになります。
php-fpm: # snip environment: OTEL_PHP_AUTOLOAD_ENABLED: true OTEL_SERVICE_NAME: app OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
PHP コード
ここではサンプルエンドポイントを実装しています。ゼロコード実装なので計装に関するコードはありません。
Route::get('/api/contents/{code}', function (string $code) { // SQL 発行 $content = DB::table('contents')->where('country_code', $code)->first(); if ($content === null) { return response()->json(['error' => 'Not Found'], 404); } // 外部 API に HTTP リクエスト送信 $flag = Http::get('https://restcountries.com/v3.1/name/' . $content->country_code) ->throw() ->json('0.flag'); return response()->json([ 'country_code' => $content->country_code, 'name' => $content->name, 'flag' => $flag, ]); });
実行
Laravel アプリケーションの上記サンプルエンドポイントにアクセスします。ここでは処理内容は本質では無いので割愛します。リクエスト処理が完了するとトレースが jaeger に記録されます。
自動計装によって、5 つのスパンが記録されています。
SQL を発行しているスパンを開くと、発行された SQL 文(プリペアードステートメント)が記録されています。
HTTP リクエストを送信しているスパンを開くと、送信先 URL やレスポンスステータスコードなどが記録されています。
このようにゼロコード計装パッケージを利用すれば、アプリケーションコードの変更なしにテレメトリデータを計装できます。
さいごに
PHP アプリケーションに OpenTelemetry を利用してトレースを計装してみました。
ここではローカル環境でのテストなので Jaeger に送信していましたが、OTel Collector を AWS が提供している ADOT Collector を変更して必要な設定を行うことで同じ PHP アプリケーションのトレースを AWS X-Ray に送信できました。こうした柔軟性が OpenTelemetry を利用するメリットですね。
PHP の他に Go アプリケーションでも OpenTelemetry SDK でトレースを計装したのですが、PHP 版 SDK を触って OpenTelemetry の知識があったおかげでスムーズに計装できました。このように一度獲得した知識が流用できるのも良い点です。
本エントリの内容は、ローカル環境で手軽に動かすことができるので試してみてください。