AWS Distro for OpenTelemetry で Laravel から AWS X-ray に trace を送ってみる

この記事は、AWS Containers Advent Calendar 2023 の 19 日目 (12/19) のエントリーです。

PHP のフレームワーク Laravel で、ADOT (AWS Distro for OpenTelemetry) を利用した trace の送信を試した備忘録です。
正直、設定の書き方がわかっていないので、正しい書き方などのツッコミをお待ちしております。

[AWS Distro for OpenTelemetry](https://aws-otel.github.io/) とは、OpenTelemetry プロジェクトの AWS がサポートするディストリビューションです。

各言語ごとの対応状況は以下にあります。
https://opentelemetry.io/docs/instrumentation/#status-and-releases
PHP が trace に対応しているため、PHP Laravel を利用して trace の送信を試してみます。

基本的には[こちらの手順](https://opentelemetry.io/docs/instrumentation/php/exporters/#otlp)を参考にして trace を AWS X-Ray に送信します。

## Laravel プロジェクト作成

Composer を使ってパッケージ/ライブラリ管理を行うため、まず Composer をインストールします。
インストール手順は [こちら](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos) にありますが、Mac だと [brew でインストール](https://formulae.brew.sh/formula/composer) することができます。
```
$ brew install composer
```

[こちらの手順](https://laravel.com/docs/10.x/installation) に沿って、Laravel のプロジェクトを作成します。
```
$ composer create-project laravel/laravel example-app
```
Laravel を起動します。
```
$ cd example-app
$ php artisan serve
```
http\://localhost:8000 にアクセスすると初期画面が表示されます。
![スクリーンショット 2023-12-17 15.19.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94838/a6b6748f-8869-abc1-e979-f3f42addc06e.png)


## OpenTelemetry の自動計装

[Automatic Instrumentation](https://opentelemetry.io/docs/instrumentation/php/automatic/) を使ってみます。
Automatic Instrumentation を利用するには、PHP 8.0 以上と、[OpenTelemetry の PHP 拡張モジュール](https://github.com/open-telemetry/opentelemetry-php-instrumentation) が必要です。

拡張モジュールは pecl、 [pickle](https://github.com/FriendsOfPHP/pickle) 、 [php-extension-installer](https://github.com/mlocati/docker-php-extension-installer) が利用可能です。
それぞれの方法での [インストール方法はこちら](https://github.com/mlocati/docker-php-extension-installer) にあります。今回は pecl でのインストールを行ってみます。
pecl は PHP 拡張モジュールのリポジトリで、 [pear](https://pear.php.net/) パッケージシステムを経由して使用可能です。
まずソースからビルドするために必要なものを設定します。
```
$ brew install gcc make autoconf
```
pecl で OpenTelemetry の PHP 拡張モジュールをビルド、インストールします。
```
$ pecl install opentelemetry
```
ここでいくつかエラーがでました。

```
Build process completed successfully
Installing '/usr/local/Cellar/php/8.3.0/pecl/20230831/opentelemetry.so'

Warning: mkdir(): File exists in System.php on line 294
PHP Warning:  mkdir(): File exists in /usr/local/Cellar/php/8.3.0/share/php/pear/System.php on line 294

Warning: mkdir(): File exists in /usr/local/Cellar/php/8.3.0/share/php/pear/System.php on line 294
ERROR: failed to mkdir /usr/local/Cellar/php/8.3.0/pecl/20230831
```

`/usr/local/Cellar/php/8.3.0/pecl/20230831` が無いようです。
確認してみると、`/usr/local/Cellar/php/8.3.0/pecl` は `/usr/local/lib/php/pecl` のシンボリックリンクになっていましたが、`pecl` というディレクトリが作られていませんでした。
```
$ cd /usr/local/lib/php/    
$ ll
total 0
lrwxr-xr-x@ 1 kajihiro  admin  39 12 17 00:32 20230831 -> ../../Cellar/php/8.3.0/lib/php/20230831
lrwxr-xr-x@ 1 kajihiro  admin  36 12 17 00:32 build -> ../../Cellar/php/8.3.0/lib/php/build
```
原因までは調べていませんが、`pecl` ディレクトリを作成し、`20230831` ディレクトリの中身をコピーしたところ正常にインストールが行われました。

php.ini に拡張モジュールを設定し、確認します
```php.ini
extension=opentelemetry.so
```
```
$ php -m | grep opentelemetry
opentelemetry
```
これで OpenTelemetry の PHP 拡張モジュールが設定されました。
しかし、 **2023年12月時点では、Automatic Instrumentation は trace に対応していない**ようです・・・
https://opentelemetry.io/docs/instrumentation/php/automatic/

> Installing the OpenTelemetry extension by itself will not generate traces. You must also install the SDK and one or more instrumentation packages for the frameworks and libraries that you are using, or alternatively write your own.


なので、trace を送る設定を手動で入れていきます。


## OpenTelemetry の手動計装

手動での trace の設定は [こちらの手順](https://opentelemetry.io/docs/instrumentation/php/manual/) で進めていきます。
PHP 用 OpenTelemetry SDK を使用するには、psr/http-client-implementation と psr/http-factory-implementation の依存関係を満たすパッケージが必要です。ここでは、両方を提供する [Guzzle](https://github.com/guzzle/guzzle) を使用します。
[PSR (PHP Standards Recommendations)](https://laravuejs.dev/tutorials/laravel-s1/laravel-115/) とは、PHP-FIG (PHP Framework Interop Group) が策定している PHP のコーディング規約です。

必要なパッケージをインストールしていきます。
```
$ composer require guzzlehttp/guzzle
```
```
$ composer require \
  open-telemetry/sdk \
  open-telemetry/exporter-otlp
```
[こちらの手順](https://opentelemetry.io/docs/instrumentation/php/manual/#traces) を見ながら trace の計装をしていきます。
```public/index.php
require __DIR__.'/../vendor/autoload.php';
use OpenTelemetry\API\Globals;
$tracerProvider = Globals::tracerProvider();
$tracer = $tracerProvider->getTracer(
  env('OTEL_SERVICE_NAME'),
  '1.0.0', //version
  env('OTEL_EXPORTER_OTLP_ENDPOINT'), //schema url
);
$span = $tracer->spanBuilder(env('OTEL_SERVICE_NAME'))->startSpan();
//do some work
$span->end();
```
※設定値を環境変数から読み込むようにしています。
が、ここでエラーが発生しています。
```
Use of unknown class: 'OpenTelemetry\\API\\Globals'
```
どうやら必要なパッケージが足りないようなので追加をします。
```
$ composer require open-telemetry/api
```

## Laravel 起動

関連するコンポーネントと一緒に Laravel を起動し、HTTP でリクエストを受けるために docker compose を利用して構築を行います。この際に trace を送るための otel-collector コンテナも一緒に起動します。
ファイル構成は以下のようになっています。
```
php
├── nginx
│   ├── Dockerfile
│   └── conf.d
│       ├── default.conf
│       └── default.conf.template
└── phpfpm
    ├── Dockerfile
    ├── example-app
    └── php.ini
```

```php/nginx/Dockerfile
FROM nginx:alpine
ADD ./conf.d /etc/nginx/conf.d

RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log
```

後に記載しますが、envsubst を使って環境変数から設定を持ってくるようにしています。
```php/nginx/conf.d/default.conf.template
server {
    listen       8080;
    charset      utf-8;

    root /var/www/html/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        
        fastcgi_pass ${LARAVEL_HOST}:9000;
        fastcgi_index index.php;
        include       fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

}
```

nginx のビルドの際に環境変数から値を取得して default.conf を生成するようにしています。
```docker-compose.yaml
version: '3'
services:
  nginx-php:
    build:
      context: ./php/nginx
      dockerfile: ./Dockerfile
    volumes:
      - ./php/phpfpm/example-app:/var/www/html
    ports:
      - 8080:8080
    depends_on:
      - phpfpm
    environment:
      LARAVEL_HOST: phpfpm
    command: sh -c "envsubst '$$LARAVEL_HOST' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
  phpfpm:
    build:
      context: ./php/phpfpm
      dockerfile: ./Dockerfile
    volumes:
      - ./php/phpfpm/example-app:/var/www/html
    ports:
      - 9000:9000
    environment:
      OTEL_PHP_AUTOLOAD_ENABLED: true
      OTEL_SERVICE_NAME: "local-laravel-app"
      OTEL_TRACES_EXPORTER: otlp
      OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
      OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318'
      OTEL_PROPAGATORS: baggage,tracecontext
  otel-collector:
    image: public.ecr.aws/aws-observability/aws-otel-collector:latest
    command: [ "--config=/etc/otel-agent-config.yaml" ]
    environment:
      - AWS_PROFILE=default
      - AWS_REGION=us-east-1
    volumes:
      - ./otel-config.yaml:/etc/otel-agent-config.yaml
      - ~/.aws/credentials:/root/.aws/credentials:ro
      - ~/.aws/config:/root/.aws/config:ro
    ports:
      - 4317:4317
      - 4318:4318
```

otel-collector が AWS X-ray の API を実行できるように credential 情報を渡しています。こちらはローカルで実行する場合の設定なので、ローカル PC に保存されている credential 情報をコンテナの volume に渡して利用しています。

    environment:
      - AWS_PROFILE=default
      - AWS_REGION=us-east-1
    volumes:
      - ./otel-config.yaml:/etc/otel-agent-config.yaml
      - ~/.aws/credentials:/root/.aws/credentials:ro
      - ~/.aws/config:/root/.aws/config:ro

コンテナを起動します。
コンテナ開発用のオープンソースクライアント「[Finch](https://aws.amazon.com/jp/blogs/news/introducing-finch-an-open-source-client-for-container-development/)」を利用しています。
```
$ finch compose -f docker-compose.yaml up　-d
```
localhost で起動している nginx にアクセスします。

http\://localhost:8080
![スクリーンショット 2023-12-19 19.31.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94838/0d610340-56f4-9980-317f-aa608c7f9a7e.png)


## Trace データの確認
AWS X-ray に trace が送られているか確認します。
![スクリーンショット 2023-12-19 19.07.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94838/056ec174-c11d-931e-3494-362ab5f3a204.png)
![スクリーンショット 2023-12-19 19.07.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94838/1f378eec-4bb7-ad55-7c59-3a828803f2e3.png)


無事 trace データが送られていました。

## まとめ

ローカルの Laravel から OpenTelemetry を利用して trace　を送る手順を試してみました。
本番環境で動かす場合 (Amazon EKS や Amazon ECS で動かす場合) は、credential や環境変数の情報を適宜 [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-manager.html) や [AWS Secrets Manager](https://aws.amazon.com/jp/secrets-manager/) に保存・利用してください。