OpenTelemetry for PHPでディープヘルスチェックがトレースされないようSamplerを自作してみた
リテールアプリ共創部@大阪の岩田です。Opentelemetryでトレースを仕込む際、ヘルスチェック用エンドポイントへのアクセスはトレース対象外にしたいという要件はあるあるだと思います。以前関わった案件ではOTEL CollectorのFilter Processorを利用してこの要件を実現していました。しかし、今関わっている案件ではDBアクセスを伴ういわゆるディープヘルスチェックが実装されており、このディープヘルスチェック用のエンドポイントに対して定期的なアクセスが発生します。単にFilter Processorを使ってHTTPのエンドポイントだけでフィルタすると、子スパンであるDBアクセス処理は依然としてトレース対象となってしまいます。
Tail Sampling Processorを使ってサンプリングしても良いのですがOTEL Collectorが複数サービスに紐づいている場合、システムAのヘルスチェック用エンドポイントは/health
でシステムBは/health-check
でシステムCは/healthcheck
で...
となると設定がカオスになっていきます。また、微々たるレベルとはいえOTEL Collector側に負荷をかけることになるのも気になります。
そこで今回はディープヘルスチェックに関して親のスパン・子のスパン共にトレース対象外とするようSDK側でSamplerを自前実装してみました。
環境
今回利用した環境は以下の通りです。
- PHP: 8.1.15
- 各種ライブラリ
- laravel/framework: v10.48.28
- open-telemetry/api: 1.2.2
- 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
- Dockerイメージ
- jaegertracing/all-in-one: 1.66.0
- otel/opentelemetry-collector: 0.119.0
やってみる
今回対象となるのはLaravel製のアプリケーションです。Laravelアプリにトレース処理を導入する手順は以下のブログでも紹介しているので参考にしてください。
まずは普通にトレース処理を実装
まずはinstrument.php
にトレース関連の処理を記述します。
<?php
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
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\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;
require __DIR__.'/../vendor/autoload.php';
$resource = ResourceInfoFactory::emptyResource()->merge(ResourceInfo::create(Attributes::create([
ResourceAttributes::SERVICE_NAMESPACE => 'demo',
ResourceAttributes::SERVICE_NAME => 'test-application',
ResourceAttributes::SERVICE_VERSION => '0.1',
ResourceAttributes::DEPLOYMENT_ENVIRONMENT_NAME => 'development',
])));
$spanExporter = new SpanExporter(
(new PsrTransportFactory())->create('http://host.docker.internal: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();
index.php
から上記のinstrument.php
をrequireします。
<?php
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
require_once './instrument.php';
...略
Docker Composeを使ってOTEL CollectorとJaegerを起動します。docker-compose.yaml
は以下の通りです。
services:
jaeger:
image: jaegertracing/all-in-one
ports:
- '16686:16686'
# 開発中にトレースデータを確認したい場合はdocker-composeの--profileにtraceを指定する
profiles:
- trace
otel-collector:
image: otel/opentelemetry-collector
ports:
- '4318:4318'
command: ['--config=/etc/otel-collector-config.yaml']
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
# 開発中にトレースデータを確認したい場合はdocker-composeの--profileにtraceを指定する
profiles:
- trace
本題から逸れますが、実際のdocker-compose.yaml
には他にも色々なサービスを定義しているのでOTEL関連のサービスにはtrace
というプロファイルを設定して必要なときだけ起動するようにしています。
バインドマウントしている設定ファイルotel-collector-config.yaml
の中身は以下の通りです。
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
processors:
batch/traces:
timeout: 1s
send_batch_size: 50
filter:
traces:
span:
- 'attributes["url.path"] == "api/health-check"'
exporters:
otlp/jaeger:
endpoint: 'jaeger:4317'
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch/traces]
# processors: [filter,batch/traces]
exporters: [otlp/jaeger]
docker compose --profile trace up
でOTEL CollectorとJaegerを起動したらヘルスチェック用エンドポイントにアクセスし、JaegerのUIを確認してみましょう。
普通にHTTPアクセスとDBアクセスの両方がトレースされています。
Filter Processorでヘルスチェック用エンドポイントへのアクセスをフィルタ
今度はOTEL CollectorのFilter Processorでヘルスチェック用エンドポイントへのアクセスをフィルタしてみましょう。processors
のコメントアウト箇所を入れ替えFilter Processorを通すようにします。
# processors: [batch/traces]
processors: [filter,batch/traces]
OTEL Collectorを再起動してヘルスチェック用エンドポイントにアクセスした後、JaegerのUIを確認します。
ヘルスチェック用エンドポイントへのHTTPアクセスはトレースされていませんが、子スパンのDBアクセス処理がトレースされています。これはノイズになるのでトレース対象外としたいところです...
Samplerを実装する
ということでPHP側にSamplerを実装してみます。ブログ執筆時点ではOTELの公式ドキュメントでPHPのSDKにはSampling
のセクションが存在しませんが普通に実装できました。
まずSamplerInterface
を実装する独自のクラスを定義します。クラスの全体は以下の通りです。
class MySampler implements SamplerInterface {
public function shouldSample(
ContextInterface $parentContext,
string $traceId,
string $spanName,
int $spanKind,
AttributesInterface $attributes,
array $links,
): SamplingResult {
$parentSpan = Span::fromContext($parentContext);
$parentSpanContext = $parentSpan->getContext();
$traceState = $parentSpanContext->getTraceState();
if (str_starts_with($attributes->get('url.path'), 'api/health-check')) {
return new SamplingResult(
SamplingResult::DROP,
[],
$traceState
);
}
return new SamplingResult(
SamplingResult::RECORD_AND_SAMPLE,
[],
$traceState
);
}
public function getDescription(): string
{
return 'MySampler';
}
}
スパンごとにshouldSample
が呼び出されるので、$attributes
からurl.path
を取得し、URLが'api/health-check'
から始まる場合はSamplingResult
のコンストラクタ引数にSamplingResult::DROP
を渡してインスタンスを生成&returnします。これでヘルスチェック用エンドポイントへのアクセスはサンプリング対象外となります。
このMySampler
クラスのインスタンスをParentBased
のコンストラクタ引数に渡してParentBased
クラスのインスタンスを生成、生成したインスタンスをSdkBuilderクラスのsetSampler
に渡します。
$tracerProvider = TracerProvider::builder()
->addSpanProcessor(
BatchSpanProcessor::builder($spanExporter)->build()
)
->setResource($resource)
->setSampler(new ParentBased(new MySampler()))
->build();
サンプリング用のクラスとしてParentBased
クラスを指定することで親スパンのサンプリング有無を子スパンに継承できます。これによって、サンプリング対象外であるヘルスチェック用エンドポイントへのアクセスの子スパンであるDBアクセス処理もサンプリング対象外となります。
修正後のinstrument.php
の全体像です。使い捨てなのでMySampler
クラスも詰め込んでますが、実際に利用する際は適宜整理してください。
<?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\Attribute\AttributesInterface;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\SamplerInterface;
use OpenTelemetry\SDK\Trace\SamplingResult;
use OpenTelemetry\SDK\Trace\Span;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;
require __DIR__.'/../vendor/autoload.php';
class MySampler implements SamplerInterface {
public function shouldSample(
ContextInterface $parentContext,
string $traceId,
string $spanName,
int $spanKind,
AttributesInterface $attributes,
array $links,
): SamplingResult {
$parentSpan = Span::fromContext($parentContext);
$parentSpanContext = $parentSpan->getContext();
$traceState = $parentSpanContext->getTraceState();
if (str_starts_with($attributes->get('url.path'), 'api/health-check')) {
return new SamplingResult(
SamplingResult::DROP,
[],
$traceState
);
}
return new SamplingResult(
SamplingResult::RECORD_AND_SAMPLE,
[],
$traceState
);
}
public function getDescription(): string
{
return 'MySampler';
}
}
$resource = ResourceInfoFactory::emptyResource()->merge(ResourceInfo::create(Attributes::create([
ResourceAttributes::SERVICE_NAMESPACE => 'demo',
ResourceAttributes::SERVICE_NAME => 'test-application',
ResourceAttributes::SERVICE_VERSION => '0.1',
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)
->setSampler(new ParentBased(new MySampler()))
->build();
Sdk::builder()
->setTracerProvider($tracerProvider)
->setPropagator(TraceContextPropagator::getInstance())
->setAutoShutdown(true)
->buildAndRegisterGlobal();
これで準備が整ったので、再度ヘルスチェック用エンドポイントにアクセスしてJaegerのUIを確認してみましょう。
何も表示されません。無事に子スパン含めてヘルスチェック用エンドポイントへのアクセスがサンプリング対象外となったようです。
まとめ
サンプリングのロジックを自前で実装する必要はありますが、OTEL Collector側でサンプリングするよりも柔軟に制御できそうなところは良いですね。要件に合わせてうまく使い分けていきたいです。