PHPでOpenTelemetryを使ってトレースを収集する

こんにちは！
12月に入ると楽しいイベントが目白押しですね。クリスマスがやって来たと思ったらすぐ冬休みに入って、大晦日にお正月。遊びに行くも良し、家でゆっくり過ごすも良し、人それぞれ楽しみ方があると思います。

しかし、そういった時期でもシステムが上手く動いてくれないとおちおち休んでいられません。普段からシステムの信頼性を高め、障害時にはすぐに復旧できる状態にしておくことが重要です。

ここでは、PHPでOpenTelemetryを使ってトレースを収集し、Google CloudのCloud Traceで可視化するまでの一連の流れをやってみます。

トレースとは何かについては、[以前書いたこちらの記事](https://qiita.com/atsu_kg/items/c3ee8141e4638957a947)の「分散トレースの重要性」「トレースの構成要素」のセクションを参照ください。

# 前提
PHP8.1
[open-telemetry/opentelemetry](https://packagist.org/packages/open-telemetry/opentelemetry) 1.0.0

# OpenTelemetry のインストール
OpenTelemetry の Composer パッケージはSDKやAPIなど細かく分割されており、必要なものだけインストールするのが推奨されますが、ここではそれらを全て含んだ `open-telemetry/opentelemetry` をインストールします。

```
php composer.phar require open-telemetry/opentelemetry "1.0.0"
```

# TracerProviderの生成
TracerProvider にはトレースに関連する設定情報を持たせます。
設定情報には、スパンをどのように収集するか、収集したスパンをどこに送信するかなどが含まれます。

```php
<?php

function createTracerProvider(): TracerProviderInterface
{
    $spanExporter = new SpanExporter(
        (new OtlpHttpTransportFactory())->create('http://otelcol:4318/v1/traces', 'application/json')
    );
    $spanProcessor = BatchSpanProcessor::builder($spanExporter)->build();
    $sampler = new AlwaysOnSampler();

    return new TracerProvider(
        [$spanProcessor],
        $sampler,
    );
}
```

- [SpanExporter](https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-exporter)
    - トレースデータの送信を担います。データ送信のプロトコルによって使用するエクスポータを選択する必要があり、標準出力に出すのであればConsoleSpanExporter、Zipkinに送信するのであればZipkinExporterを使います。
    - ここでは、後述する OpenTelemetry Collector にHTTP経由で送信したいので、`OtlpHttpTransportFactory`を使って SpanExporter を組み立てています。

- [Span processor](https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-processor)
    - スパンの開始と終了のフックを可能にするものです。ビルトインのものにはBatchingSpanProcessorとSimpleSpanProcessorがあり、前者は逐次処理で後者はバッチ処理でスパンをエクスポート可能な表現に変換し、Span Exporterに渡します。
    - この2つの実装を見ると、スパンの開始時には何も行わず終了時にスパンをエクスポートしています。

- [Sampler](https://opentelemetry.io/docs/specs/otel/trace/sdk/#sampling)
    - 全データを収集するとコストやシステムパフォーマンスへの影響が懸念されるケースにおいて、データを収集するか否かのロジックを組み込みたいです。そのロジックを担うのが Sampler です。
    - ビルトインのものにはTraceIdRatioBasedSamplerやAlwaysOnSamplerなどがあり、前者は指定したサンプリングレート(確率)に基づいてサンプリングし、後者は常にサンプリングを行います。
    - ここでは実行したスクリプトのトレースを常に確認したいので、`AlwaysOnSampler`を使用します。

# Spanの生成
先ほど生成したTracerProviderからTracerを生成し、Tracerを使ってSpanを生成・開始します。
そして何らかの処理（以下例では`sleep(1)`）を行なった後、Spanを終了します。<br>
スパンの開始と終了をもって1つのスパンになります。<br>
処理の終了時にTraceProviderをシャットダウンして、エクスポートされずに残っているスパンをエクスポートします。

```php
<?php

$tracerProvider = createTracerProvider();
$tracer = $tracerProvider->getTracer('io.opentelemetry.contrib.php');
$span = $tracer->spanBuilder("my span")->startSpan();
sleep(1);
$span->end();
$tracerProvider->shutdown();
```

# ネストしたSpanの生成
ネストした処理をスパンで表現したい時は、親スパンを activate し、子スパンを終了した後に親スパンを終了させます。

```php
<?php

$tracerProvider = createTracerProvider();
$tracer = $tracerProvider->getTracer('io.opentelemetry.contrib.php');
$rootSpan = $tracer->spanBuilder('root')->startSpan();
sleep(1);
// 親スパンをactivate
$parentScope = $rootSpan->activate();
// ネストした処理に$tracerを渡す
child($tracer);
// 親スパンを終了
$rootSpan->end();
$parentScope->detach();
$tracerProvider->shutdown();

function child(TracerInterface $tracer): void
{
    $childSpan = $tracer->spanBuilder('child')->startSpan();
    sleep(2);
    $childSpan->end();
}
```

このスクリプトは以下のようなトレースを収集します。
ルートスパンを開始して1秒後に子スパンが開始され、その2秒後に子スパンとルートスパンが終了しています。

<img width="412" alt="スクリーンショット 2023-12-11 11.04.47.png (20.9 kB)" src="https://img.esa.io/uploads/production/attachments/17300/2023/12/11/63938/f8f55103-9c4e-44bd-aada-3857b4848b64.png">


ここまでで、トレースを収集して送信するためのPHPスクリプトを準備できました。<br>
次に、収集したトレースを送信する方法を見ていきます。

# テレメトリーデータの送信方法
テレメトリーデータを送信する方法は2つあります。

### 1. アプリケーションから直接送信する
データ送信先がPHP用のExporterを提供している場合、それをTracerProviderで設定すると、収集したトレースをアプリケーションが直接送信できます。
データ送信先である各ベンダーは言語毎にExporterを作成しなければいけないため、Exporterが無い言語もあります。

本記事ではCloud Trace にデータを送信したいのですが、記事執筆時点ではPHP用のExporterが無いため、そのような場合は2つ目の方法を取ります。

### 2. OpenTelemetry Collector を介して送信する
[OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) とは、テレメトリーデータを収集・送信するためのエージェントです（以下 Collector と呼びます）。
アプリケーションはデータをまず Collector に送信し、Collector がトレースに対応したベンダーに送信します。

この方法の利点は、アプリケーション特有の計装ロジックのみをアプリケーション側に残して、それ以外のExporterの設定や共通で行いたいデータ処理を Collector に切り出せることが挙げられます。<br>
デメリットは、アプリケーションから直接送信するのに比べて1レイヤー増えることです。

# OpenTelemetry Collector の構築
今回は[OpenTelemetry Collector Releases](https://github.com/open-telemetry/opentelemetry-collector-releases/pkgs/container/opentelemetry-collector-releases%2Fopentelemetry-collector-contrib)でリリースされているものから opeltelemetry-collector-contrib のイメージを使用します。
これはCollectorで使用可能なさまざまなコンポーネントが含まれているため検証用に便利なものですが、本番運用の際には[カスタムコレクター](https://opentelemetry.io/docs/collector/custom-collector/)を構築するのが推奨されます。

まずCollector の設定ファイルをconfig.yamlとして用意します。

```yaml
receivers:
  otlp:
    protocols:
      http:

exporters:
  googlecloud:

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [googlecloud]
```


- receivers
    - レシーバはデータを Collector に取り込むためのものです。ここでは[OpenTelemetry Protocol(OTLP)](https://opentelemetry.io/docs/specs/otel/protocol/)形式のデータをHTTP経由で取り込むよう設定しています。

- exporters
    - エクスポーターはテレメトリーデータをバックエンドに送信するためのものです。ここでは Google Cloud を送信先に設定しています。

- service
    - サービスは receiver と exporter で設定した構成を使って、トレースデータのパイプラインを定義します。

その他、設定の詳細は[公式ドキュメント](https://opentelemetry.io/docs/collector/configuration/)を参照ください。


次に、以下のようなDockerfileを用意して Collector コンテナを立ち上げます。

```Dockerfile
FROM ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.90.1-amd64
COPY config.yaml /etc/otel/config.yaml
COPY credential.json /etc/otel/credential.json
ENV GOOGLE_APPLICATION_CREDENTIALS="/etc/otel/credential.json"
EXPOSE 4318
CMD ["--config", "/etc/otel/config.yaml"]
```

先ほど作った config.yaml をコンテナに配置しています。<br>
また、Google Cloud のサービスアカウントキーファイル（credential.json）を配置していますが、本番運用では、[サービスアカウントキーを管理するためのベストプラクティス](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys?hl=ja)に則って管理してください。

# Cloud Trace での確認
Collectorコンテナが立ち上がったことを確認し、作成したPHPスクリプトを実行すると、Cloud Trace に収集したトレースを表示することができます！

<img width="1306" alt="スクリーンショット 2023-12-08 19.04.03.png (64.9 kB)" src="https://img.esa.io/uploads/production/attachments/17300/2023/12/08/63938/16c404fa-ae9a-4478-b9ef-1f480e707937.png">


# まとめ
PHP で OpenTelmetry を使ってトレースを収集し、Cloud Traceで可視化する方法を見てきました。<br>
このようにトレースを活用することでオブザーバビリティを高め、システムの挙動を確認できると良いですね。