OpenTelemetry Laravel auto-instrumentationが自動設定したスパンをカスタマイズしてトレースマップがダウンストリームサービス毎に分割されるよう調整してみた
リテールアプリ共創部@大阪の岩田です。
以前こんなブログを書きました。
これと類似の話ですが、OpenTelemetry Laravel auto-instrumentationを使った場合、外部サービスに対するHTTPリクエストのスパン名はGETやPOSTなどHTTPメソッド名となります。
※OpenTelemetry Guzzle auto-instrumentationやOpenTelemetry PSR-15 auto-instrumentationも同様の挙動になるようです。
Laravelアプリケーションが外部サービスAと外部サービスBのAPIを呼び出している場合、トレースマップ上ではサービスAとサービスBでノードを分割して表示したいところですが、上記の仕様によってノードはGETという単一のノードとして表示されたり、GETとPOSTという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.jpとclassmethod.jpにHTTPリクエストを発行しています。このメソッドを呼び出した際にトレースマップの表示がどのように変わるか確認します。
自動インストルメンテーションにお任せ
まずはOpenTelemetry Laravel auto-instrumentationにお任せするパターンです。以下ブログと同様の手順でインストルメンテーションしています。
結果は以下の通りです。

HTTPリクエストの送信先はdev.classmethod.jpとclassmethod.jpの2つですが、トレースマップ上のノードはGETという1つのノードで表現されています。
おそらくここでHTTPメソッドをスパン名に設定しています。
この状態ではダウンストリームの2システムdev.classmethod.jpとclassmethod.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にリクエストを送ったあとにトレース結果を確認してみます。

今度はエンドポイント毎にノードが別れて表示されました!
これでトレースマップから各ダウンストリームシステムごとのメトリクスも確認できるようになり、オブザーバビリティが向上しました!

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








