Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

PHP アプリケーションのトレース計装ではじめる OpenTelemetry 入門

OpenTelemetry を利用して PHP アプリケーションのテレメトリデータを計装する方法をまとめました。

本エントリのコードは下記で公開しています。 github.com

OpenTelemetry とは

opentelemetry.io

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 用語集を参照してください。

opentelemetry.io

PHP アプリケーションのマニュアル計装(手動計装)

PHP アプリケーションから OpenTelemetry SDK を利用してトレースを計装します。

構成

OTel Collector

PHP アプリケーションからテレメトリデータ(ここではトレース)を OpenTelemetry Collector(OTel Collector) に送信します。OTel Collector は、OpenTelemetry で提供されているツールで、アプリケーションなどからテレメトリデータを受信し、必要であればデータの加工などを行い、Jaeger のような分析ツールに送信します。

github.com

PHP アプリケーションから直接分析ツールに送信することも可能ですが、OTel Collector をリレーすることで PHP コードでは OTel Collector に送信する役割のみを担い、その後の送信先の指定や送信先に合わせたプロトコル変換、データの加工などは OTel Collector に委ねるという構成が可能になります。OTel Collector はバッファリングしたり、利用メモリの上限を設定するなどの制御が可能なので、アプリケーション影響を最低限にして安定的にテレメトリデータを送信するという観点でも有用です。

アプリケーションの主たる関心事はテレメトリデータの送信ではないので、不要な責務を持たないよう OTel Collector を利用するのが良いと考えています。

OpenTelemetry Collector(OTel Collector)

OpenTelemetry 公式で配布されている OTel Collector には以下の 3 種類があります。

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 コンテナが配布されており、ローカル環境でも手軽に利用できます。

www.jaegertracing.io

動作環境

ここでは 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

設定項目については下記にまとまっています。

実行

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 拡張

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 の知識があったおかげでスムーズに計装できました。このように一度獲得した知識が流用できるのも良い点です。

本エントリの内容は、ローカル環境で手軽に動かすことができるので試してみてください。

参照

opentelemetry.io

opentelemetry.io

learningopentelemetry.com

www.shkuro.com