OpenTelemetryでEC2上のLaravelアプリケーションをトレースしてみた

OpenTelemetryでEC2上のLaravelアプリケーションをトレースしてみた

EC2 × Laravel × OpenTelemetryの環境に一歩一歩段階を踏みながらトレースを実装してみました。
Clock Icon2025.02.19

リテールアプリ共創部@大阪の岩田です。私事ですが最近チームの異動があり7~8年ぶりにPHPと戯れています。
その中でLaravelにOpenTelemetryのトレースを仕込む機会があったので、検証がてらEC2(Cloud9)上にLaravel Breezeの環境を作ってX-Rayでトレースできるように色々調整してみました。

環境

今回利用した環境は以下の通りです。

  • PHP: 8.3.10
  • 各種ライブラリ
    • laravel/framework: v11.42.1
    • open-telemetry/api: 1.2.2
    • open-telemetry/contrib-aws: 1.0.0beta12
    • open-telemetry/exporter-otlp: 1.2.0
    • open-telemetry/opentelemetry-auto-laravel: 1.0.1
    • open-telemetry/sdk: 1.2.2
  • OpenTelemetry auto-instrumentation extension: 1.1.2
  • ADOT CollectorのDockerイメージ: public.ecr.aws/aws-observability/aws-otel-collector:v0.41.2

やってみる

まずはLaravel公式ドキュメントの手順に従ってコマンドを叩いてLaravel Breezeの環境を作ります。

composer global require laravel/installer
laravel new example-app
cd example-app
npm install && npm run build
composer run dev

詳細は割愛しますが別途ALBとRDSも用意し、ALB → EC2(Cloud9) → RDSと通信できる環境を用意します。

ALB経由でLaravel Breezeの画面が表示できたら一旦OKです。

Laravel Breezeの画面

まずは自動計装を試してみる

それではさっそくトレースを仕込んでいきましょう!まずはミニマムに自動計装から試してみます。

まずはPHPのExtensionをインストールします。

pecl install opentelemetry

続いてOTEL関連のライブラリをインストールします。基本的なライブラリ類に加えて open-telemetry/opentelemetry-auto-laravelをインストールしています。これでLaravel製のアプリが簡単に計装できるようになります。

composer require open-telemetry/api \
    open-telemetry/exporter-otlp \
    open-telemetry/opentelemetry-auto-laravel \
    open-telemetry/sdk

続いてLaravelからトレースデータをエクスポートする先としてADOT Collectorを用意します。以下のファイルを用意し、docker compose up -d でADOT Collectorを起動しましょう。

docker-compose.yaml
services:
  otel-collector:
    image: public.ecr.aws/aws-observability/aws-otel-collector:v0.41.2
    command: ["--config", "/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - '4318:4318'

バインドマウントしている設定ファイルは以下の通りです。

otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
exporters:
  otlphttp:
    traces_endpoint: https://xray.ap-northeast-1.amazonaws.com/v1/traces
    compression: gzip
    auth:
      authenticator: sigv4auth
extensions:
  sigv4auth:
    region: ap-northeast-1
    service: xray
service:
  extensions: [sigv4auth]
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp]

今回はX-Rayの OTLPエンドポイントにエクスポートする構成としています。X-RayのOTLPエンドポイントはTransaction Searchの有効化が必須なので、この辺りは要件に応じてotlphttpの代わりにawsxrayを指定するなど適宜調整してください。

https://dev.classmethod.jp/articles/x-ray-support-otlp-endpoint/

Laravelの.envにOTEL関係の環境変数を設定します。

.env
...略

OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=laravel
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
OTEL_PROPAGATORS=baggage,tracecontext
OTEL_LOGS_EXPORTER=none

ここまでで準備完了です。Laravel Breezeを再起動して適当にアクセスしてみましょう。しばらくすると無事にX-Rayのトレース結果が確認できました。

自動計装のトレース結果

コードの修正無しで簡単にトレースできて楽チンですね!

PHPコードを修正してメタデータを調整してみる

とりあえず自動計装はできるようになりましたが、実案件のことを考えるともう少し色々と情報が欲しくなると思います。今後の拡張を考慮し、PHPのコードでもう少し細かく設定していきます。

まずinstrument.phpというファイルを作成し、以下の内容を記述します。

instrument.php
<?php
require_once __DIR__.'/vendor/autoload.php';

use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;

$resource = ResourceInfoFactory::emptyResource()->merge(ResourceInfo::create(Attributes::create([
    ResourceAttributes::SERVICE_NAMESPACE => 'cm-iwata',
    ResourceAttributes::SERVICE_NAME => 'laravel',
    ResourceAttributes::SERVICE_VERSION => 'manual-instrument',
    ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => 'development',
])));
$spanExporter = new SpanExporter(
    (new PsrTransportFactory())->create('http://localhost:4318/v1/traces', 'application/json')
);

$tracerProvider = TracerProvider::builder()
    ->addSpanProcessor(
        BatchSpanProcessor::builder($spanExporter)->build()
    )
    ->setResource($resource)
    ->build();

Sdk::builder()
    ->setTracerProvider($tracerProvider)
    ->setPropagator(TraceContextPropagator::getInstance())
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

追加したinstrument.phprequre_onceするようapp/bootstrap.phpを修正します。

app/bootstrap.php
...use Illuminate\Foundation\Configuration\Middleware;

require_once __DIR__ . '/../instrument.php';

return Application::configure(basePath: dirname(__DIR__))
...

PHPのコードで色々していするようになったので、.envからはOTEL関連の環境変数を削除します。

.env
#OTEL_PHP_AUTOLOAD_ENABLED=true
#OTEL_SERVICE_NAME=laravel
#OTEL_TRACES_EXPORTER=otlp
#OTEL_EXPORTER_OTLP_PROTOCOL=http/json
#OTEL_EXPORTER_OTLP_ENDPOINT=http://172.31.17.106:4318
#OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://172.31.17.106:4318/v1/traces
#OTEL_PROPAGATORS=baggage,tracecontext
#OTEL_LOGS_EXPORTER=none

ここまでできたらLaravel Breezeを再起動しトレース結果を確認してみましょう。

メタデータ調整後のトレース結果

微妙な違いなので分かりづらいですが、メタデータにPHPのコードで指定した値が反映されているのが分かります。

Detectorを使ってEC2の情報を追加してみる

今回Laravelの実行環境としてEC2(Cloud9)を利用しているので、今度はDetectorを使ってEC2の情報もトレースデータに埋め込んでみます。

まずはADOTのSDKをインストールします。

composer require open-telemetry/contrib-aws:1.0.0beta12

※まだベータ版なので実案件で利用する際はご注意ください。

続いてinstrument.phpを修正しEC2環境固有の情報を付与するようにします。

instrument.php
-use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
 use OpenTelemetry\SDK\Sdk;
 use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
 use OpenTelemetry\SDK\Trace\TracerProvider;
 use OpenTelemetry\SemConv\ResourceAttributes;
 use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;

+use GuzzleHttp\Client;
+use GuzzleHttp\Psr7\HttpFactory;
+use OpenTelemetry\Aws\Ec2\Detector as Ec2Detector;

-$resource = ResourceInfoFactory::emptyResource()->merge(ResourceInfo::create(Attributes::create([
+$client = new Client();
+$requestFactory = new HttpFactory();
+$detector = new Ec2Detector($client, $requestFactory);
+
+$resource = $detector->getResource()->merge(ResourceInfo::create(Attributes::create([
     ResourceAttributes::SERVICE_NAMESPACE => 'cm-iwata',
     ResourceAttributes::SERVICE_NAME => 'laravel',
     ResourceAttributes::SERVICE_VERSION => 'manual-instrument',

修正後したinstrument.phpの全体像はこちらです。

instrument.php
<?php
require_once __DIR__.'/vendor/autoload.php';

use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use OpenTelemetry\Aws\Ec2\Detector as Ec2Detector;

$client = new Client();
$requestFactory = new HttpFactory();
$detector = new Ec2Detector($client, $requestFactory);

$resource = $detector->getResource()->merge(ResourceInfo::create(Attributes::create([
    ResourceAttributes::SERVICE_NAMESPACE => 'cm-iwata',
    ResourceAttributes::SERVICE_NAME => 'laravel',
    ResourceAttributes::SERVICE_VERSION => 'manual-instrument',
    ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => 'development',
])));
$spanExporter = new SpanExporter(
    (new PsrTransportFactory())->create('http://localhost:4318/v1/traces', 'application/json')
);

$tracerProvider = TracerProvider::builder()
    ->addSpanProcessor(
        BatchSpanProcessor::builder($spanExporter)->build()
    )
    ->setResource($resource)
    ->build();

Sdk::builder()
    ->setTracerProvider($tracerProvider)
    ->setPropagator(TraceContextPropagator::getInstance())
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

これで準備完了です。Laravel Breezeを再起動しトレース結果を確認してみましょう。

Detector設定後のトレース結果

リソースの情報としてインスタンスIDやAMI IDが付与されていることが分かります。これで有事の調査も捗りそうですね。

X-RayのトレースIDを伝搬させてみる

ここまでで大分トレースデータが取得できるようになりました。今度はちょっと趣向を変えてX-Rayのトレースを有効化したLambdaからLaravel Breezeにアクセスしてみましょう。以下のコードを実行してみます。

from urllib import request
import json
import os

def lambda_handler(event, context):
    headers={
        "X-Amzn-Trace-Id": os.environ['_X_AMZN_TRACE_ID']
    }
    req = request.Request('http://<ALB名>.ap-northeast-1.elb.amazonaws.com/login',headers=headers)

    with request.urlopen(req) as res:
        status = res.getcode()
        print(status)

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

しばらくしてからサービスマップを見ると以下の通りでした。ALBにリクエストする際X-Amzn-Trace-Idを設定しているものの、サービスマップ上LamdbaとLaravelがつながっておらずトレースが伝搬できていないことが分かります。

トレースID伝搬前のトレースマップ

レースID伝搬前のトレース詳細

ということでPHPのコードを修正し、X-Ray向けのPropagatorを導入します。

instrument.php
-use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
 use OpenTelemetry\Contrib\Otlp\SpanExporter;
 use OpenTelemetry\SDK\Common\Attribute\Attributes;
 use OpenTelemetry\SDK\Resource\ResourceInfo;
@@ -15,10 +14,12 @@
 use GuzzleHttp\Client;
 use GuzzleHttp\Psr7\HttpFactory;
 use OpenTelemetry\Aws\Ec2\Detector as Ec2Detector;
+use OpenTelemetry\Aws\Xray\Propagator as XrayPropagator;

 $client = new Client();
 $requestFactory = new HttpFactory();
 $detector = new Ec2Detector($client, $requestFactory);
+$propagator = new XrayPropagator();

 $resource = $detector->getResource()->merge(ResourceInfo::create(Attributes::create([
     ResourceAttributes::SERVICE_NAMESPACE => 'cm-iwata',
@@ -39,6 +40,6 @@

 Sdk::builder()
     ->setTracerProvider($tracerProvider)
-    ->setPropagator(TraceContextPropagator::getInstance())
+    ->setPropagator($propagator)
     ->setAutoShutdown(true)
     ->buildAndRegisterGlobal();

修正後のinstrument.phpの全体像です。

instrument.php
<?php
require_once __DIR__.'/vendor/autoload.php';

use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use OpenTelemetry\Aws\Ec2\Detector as Ec2Detector;
use OpenTelemetry\Aws\Xray\Propagator as XrayPropagator;

$client = new Client();
$requestFactory = new HttpFactory();
$detector = new Ec2Detector($client, $requestFactory);
$propagator = new XrayPropagator();

$resource = $detector->getResource()->merge(ResourceInfo::create(Attributes::create([
    ResourceAttributes::SERVICE_NAMESPACE => 'cm-iwata',
    ResourceAttributes::SERVICE_NAME => 'laravel',
    ResourceAttributes::SERVICE_VERSION => 'manual-instrument',
    ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => 'development',
])));
$spanExporter = new SpanExporter(
    (new PsrTransportFactory())->create('http://localhost:4318/v1/traces', 'application/json')
);

$tracerProvider = TracerProvider::builder()
    ->addSpanProcessor(
        BatchSpanProcessor::builder($spanExporter)->build()
    )
    ->setResource($resource)
    ->build();

Sdk::builder()
    ->setTracerProvider($tracerProvider)
    ->setPropagator($propagator)
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

この状態で再度Lambdaを実行し、トレースマップを確認してみます。

トレースID伝搬後のトレースマップ

トレースID伝搬後のトレース詳細

今度は無事にLambdaとLaravelのトレースがつながりました!やったぜ!

まとめ

EC2 × Laravel × OpenTelemetryによるトレースの実装でした。

PHPでOpenTelemetryを使うのは初めてでしたが、OpenTelemetryは各言語共通の仕様が定められていることもあり割とすんなりと導入できた印象です。早くopen-telemetry/contrib-awsのbetaが外れることに期待したいです。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.