OpenTelemetry Laravel auto-instrumentationが自動設定したスパンをカスタマイズしてトレースマップがダウンストリームサービス毎に分割されるよう調整してみた

OpenTelemetry Laravel auto-instrumentationが自動設定したスパンをカスタマイズしてトレースマップがダウンストリームサービス毎に分割されるよう調整してみた

OpenTelemetry Laravel auto-instrumentationの自動インストルメンテーションでは外部サービスへのHTTPリクエストのスパン名がHTTPメソッド名になってしまい、トレースマップ上で複数の外部サービスが単一ノードとして表示されるという課題があります。 この課題を解決するため独自のSpanProcessorを実装してスパン名をカスタマイズしてみました。
2025.11.06

リテールアプリ共創部@大阪の岩田です。

以前こんなブログを書きました。

https://dev.classmethod.jp/articles/custome-otel-laravel-auto-instrumentation-span-name/

これと類似の話ですが、OpenTelemetry Laravel auto-instrumentationを使った場合、外部サービスに対するHTTPリクエストのスパン名はGETPOSTなどHTTPメソッド名となります。
OpenTelemetry Guzzle auto-instrumentationOpenTelemetry PSR-15 auto-instrumentationも同様の挙動になるようです。

Laravelアプリケーションが外部サービスAと外部サービスBのAPIを呼び出している場合、トレースマップ上ではサービスAとサービスBでノードを分割して表示したいところですが、上記の仕様によってノードはGETという単一のノードとして表示されたり、GETPOSTという2ノードとして表示されたりします。これではダウンストリームシステムの健全性を正しく評価できないので、スパンをカスタマイズして外部サービスのURLごとにノードが分割表示されるように調整してみました。

環境

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

  • PHP: 8.1.15

  • 各種ライブラリ

    • laravel/framework: v10.40.00
    • open-telemetry/api: 1.4.0
    • open-telemetry/exporter-otlp: 1.2.0
    • open-telemetry/opentelemetry-auto-laravel: 1.2.0
    • open-telemetry/sdk: 1.6.0
    • OpenTelemetry auto-instrumentation extension: 1.1.3

やってみる

実際に動作を確認していきましょう。まず検証用のLaravelアプリケーションに適当なRouteとControllerを追加し以下のようなメソッドを追加します。

public function index()
    {
        Http::get('https://dev.classmethod.jp');
        Http::get('https://classmethod.jp/');
        // ...略
    }

このメソッド内ではdev.classmethod.jpclassmethod.jpにHTTPリクエストを発行しています。このメソッドを呼び出した際にトレースマップの表示がどのように変わるか確認します。

自動インストルメンテーションにお任せ

まずはOpenTelemetry Laravel auto-instrumentationにお任せするパターンです。以下ブログと同様の手順でインストルメンテーションしています。

https://dev.classmethod.jp/articles/trace-laravel-on-ec2-by-opentelemetry/

結果は以下の通りです。

自動インストルメンテーションしただけのトレース結果.jpg

HTTPリクエストの送信先はdev.classmethod.jpclassmethod.jpの2つですが、トレースマップ上のノードはGETという1つのノードで表現されています。

おそらくここでHTTPメソッドをスパン名に設定しています。

https://github.com/opentelemetry-php/contrib-auto-laravel/blob/fa6e0c9189cf559a5d83983bc313eea16d2b6b52/src/Watchers/ClientRequestWatcher.php#L56

この状態ではダウンストリームの2システムdev.classmethod.jpclassmethod.jpそれぞれの健全性が分かりづらいです。

独自のSpanProcessorを実装する

ということで以前のブログと同様にスパンの属性をカスタマイズするための独自クラスを作りました。今回は以下の実装になりました。

class MyProcessor implements SpanProcessorInterface
{
    public function onStart(ReadWriteSpanInterface $span, ContextInterface $parentContext): void
    {

        if ($span->toSpanData()->getAttributes()->has('http.request.method') && $span->getKind() === SpanKind::KIND_CLIENT) {
            $span->updateName(
                $span->toSpanData()->getAttributes()->get('server.address')
            );
        }
    }

    public function onEnd(ReadableSpanInterface $span): void
    {
    }

    public function forceFlush(?CancellationInterface $cancellation = null): bool
    {
        return true;
    }

    public function shutdown(?CancellationInterface $cancellation = null): bool
    {
        return true;
    }
}

onStartの中でhttp.request.methodというAttributeの存在をチェックし、存在する場合はHTTPリクエスト関連のスパンと判断します。さらに、Laravelアプリケーションがサーバーとしてクライアントの要求を処理するパターンと区別するためSpan kindがCLIENTであるかチェックします。チェック結果がtrueだった場合はLaravelがダウンストリームシステムにHTTPリクエストを発行するスパンと判断できるので、server.addressでスパン名を上書きしています。

OpenTelemetryの仕様書によるとserver.addressは必須項目なのでスパン名に流用して問題なさそうですね。

Semantic conventions for HTTP spans | OpenTelemetry

この独自SpanProcessorを利用するようにした処理のまとめです。この実装をapp.phpの先頭でrequire_onceすれば完成です。

<?php

require_once __DIR__.'/vendor/autoload.php';

use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;
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\Trace\SpanProcessorInterface;
use OpenTelemetry\SDK\Trace\ReadableSpanInterface;
use OpenTelemetry\SDK\Trace\ReadWriteSpanInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\API\Trace\SpanKind;


class MyProcessor implements SpanProcessorInterface
{
    public function onStart(ReadWriteSpanInterface $span, ContextInterface $parentContext): void
    {

        if ($span->toSpanData()->getAttributes()->has('http.request.method') && $span->getKind() === SpanKind::KIND_CLIENT) {
            $span->updateName(
                $span->toSpanData()->getAttributes()->get('server.address')
            );
        }
    }

    public function onEnd(ReadableSpanInterface $span): void
    {
    }

    public function forceFlush(?CancellationInterface $cancellation = null): bool
    {
        return true;
    }

    public function shutdown(?CancellationInterface $cancellation = null): bool
    {
        return true;
    }
}


$resource = ResourceInfo::create(Attributes::create([
    ResourceAttributes::SERVICE_NAME => 'laravel-app',
    ResourceAttributes::SERVICE_VERSION => env('OTEL_SERVICE_VERSION', '0.1'),
    ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => env('APP_ENV', 'development'),
]));

$spanExporter = new SpanExporter(
    (new PsrTransportFactory())->create(env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', 'http://otel-collector:4318/v1/traces'), 'application/json')
);

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

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

改めてLaravelにリクエストを送ったあとにトレース結果を確認してみます。

手動でスパン名を調整したトレース結果.jpg

今度はエンドポイント毎にノードが別れて表示されました!

これでトレースマップから各ダウンストリームシステムごとのメトリクスも確認できるようになり、オブザーバビリティが向上しました!
手動でスパン名を調整した後のトレースマップ.jpg

まとめ

前回のブログに引き続きOpenTelemetry Laravel auto-instrumentationが自動設定したスパンの名前をカスタマイズしてみました。やはり自動インストルメンテーションだけだと痒いところに手が届かない部分は出てくるのでこういったテクニックは有効活用していきたいですね。

参考

この記事をシェアする

FacebookHatena blogX

関連記事