WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

PHPでOpenTelemetryを使ってメトリクスを収集する

こんにちは!ホワイトプラスのコアシステム開発グループでエンジニアをやっている古賀です。
以前、PHPでOpenTelemetryを使ってトレースを取得する方法を紹介しました。
今回はメトリクス編ということで、OpenTelemetryでメトリクス(リクエストレイテンシ)を収集し、OpenTelemetry Collectorを介してGoogle Cloudに送信する方法を紹介します。

スクリーンショット 2024-01-04 14.39.27.png (59.1 kB)

前提

PHP:8.1
open-telemetry/opentelemetry:1.0.0

メトリクスとは

メトリクスはデータの数値表現で、CPU使用率やリクエストレイテンシなどが挙げられます。メトリクスを取得して監視やアラート、傾向分析に利用します。

メトリクスのデータモデルには複数のタイプがありますが、本稿ではリクエストレイテンシを可視化するためにHistogramを用います。

Histogram

ヒストグラムは、測定範囲全体を一連の間隔 (バケットと呼ばれる) に分割し、各バケットに含まれる測定値の数をカウントすることで集計します。
これだけだとイメージが湧きづらいため、具体的なカウント方法を見ていきます。

あるアプリケーションのリクエストレイテンシは、遅い時で3sくらいかかっているとします。 プロダクトオーナーは、いつ、どれだけのリクエストが、どれくらい時間がかかっているか、を把握したいと考えており、その一歩としてリクエストレイテンシ(以下レイテンシ)を観察します。

0秒から4秒の間で1秒間隔のレイテンシ分布を見たいというプロダクトオーナーの要望を元に、0秒以上1秒未満、1秒以上2秒未満、2秒以上3秒未満、3秒以上4秒未満の4つのバケットを用意します。
例えば、1:00から1:01の間に測定された7個のレイテンシが「0.7秒、0.9秒、1.1秒、1.3秒、1.5秒、2.1秒、3.7秒」だとすると、各バケットに含まれる測定値は次表のようにカウントされます。

スクリーンショット 2024-01-04 19.19.48.png (59.6 kB)

1:01以降も測定を続けることで、以下のように1分間隔でバケットカウントが得られます。

上記のデータを元にヒートマップ(Cloud Monitoringの例)で可視化してみます。

スクリーンショット 2024-01-04 16.22.37.png (48.2 kB)

X 軸が時間、Y 軸がバケット、色はバケットカウントを表しており、色が明るいほどバケットカウントが多いことを示し、色が暗いほどバケットカウントが少ないことを示しています。
この例では、最小バケットカウント(0)を黒、最大バケットカウント(10)を黄色で表し、赤とオレンジはその中間値を表します。

このように、ヒストグラム形式のデータをヒートマップで可視化することで、傾向を見て取りやすくなります。

OpenTelemetry のインストール

インストール方法は前回の記事と同じため割愛します。

セットアップ

セットアップで必要なことは大きく分けて2つあり、メトリクスに関する設定を担う MeterProviderの作成と登録です。

MeterProviderの作成

MeterProviderをある程度デフォルトの設定で使う場合は、MeterProviderBuilderこんな感じに呼び出すことにより数行で作成できます。

しかし、色々とユーザ側で設定したい場合は以下のようにMeterProviderのコンストラクタを呼び出して作成します。 今回は ExportingReaderResourceInfoCriteriaViewRegistryをユーザ側で設定し、それ以外は MeterProviderBuilder::build()で設定しているものを使用します。

<?php

function createMeterProvider(): MeterProviderInterface
{
    $reader = new ExportingReader(
        new MetricExporter(
            PsrTransportFactory::discover()->create(
                'http://otelcol:4318/v1/metrics',
                ContentTypes::JSON
            ),
            Temporality::DELTA,
        )
    );
    $resource = ResourceInfo::create(
        Attributes::create(
            [
                ResourceAttributes::SERVICE_NAME => 'test-application',
                ResourceAttributes::SERVICE_VERSION => '1.0',
                'project_id' => 'sample-project',
                'location' => 'asia-northeast1',
                'cluster' => (string)random_int(1, 100),
                'namespace' => (string)random_int(1, 100),
                'job' => random_int(1, 100),
                'instance' => random_int(1, 100),

            ]
        )
    );
    $views = (new CriteriaViewRegistry());
    $views->register(
        // 条件文をスキップ
        new class implements SelectionCriteriaInterface {
            public function accepts(
                Instrument $instrument,
                InstrumentationScopeInterface $instrumentationScope
            ): bool {
                return true;
            }
        },
        // バケット定義
        ViewTemplate::create()
            ->withName('request_latency')
            ->withAggregation(new ExplicitBucketHistogramAggregation([0, 1, 2, 3, 4])),
    );

    return new MeterProvider(
        null,
        $resource,
        ClockFactory::getDefault(),
        Attributes::factory(),
        new InstrumentationScopeFactory(Attributes::factory()),
        [$reader],
        $views,
        new WithSampledTraceExemplarFilter(),
        new NoopStalenessHandlerFactory(),
    );
}
  • ExportingReader

    • Exporterに送信するデータを渡すクラス。
    • OpenTelemetry CollectorにJSON形式で送信するためのMetricExporterを使用しています。
  • ResourceInfo

    • Resourceを表現するクラス。Resourceはテレメトリデータを生成するものに関する情報をキーバリュー形式で格納したコレクションです。
    • 今回テレメトリデータを生成するのはPHPアプリケーションになるため、アプリケーションに関する情報としてサービス名(test-application)やサービスのバージョン(1.0)を設定しています。
    • また、メトリクスをGoogle Cloud Managed Prometheusに送信する際、データの競合が発生して送信エラーになるのを防ぐため、{project_id, location, cluster, namespace, job, instance}の組み合わせで一意になるように random_int を使って値を指定しています。
  • CriteriaViewRegistry

    • メトリクスをカスタマイズする機能にViewというものがあります。CriteriaViewRegistryは条件とViewを受け取り、条件に合致する場合にViewを使ってメトリクスをカスタマイズします。
    • ここでは、条件文をスキップし(常にtrueを返す)、ヒストグラムのバケットを定義するために[0, 1, 2, 3, 4]を設定しています。

MeterProviderの登録

次に、先ほど作成したMeterProviderを登録します。
Sdk::builder()から返される SdkBuilderは、MeterProviderを含む各種Providerを登録します。 登録すると、任意の場所でMeterProviderを取得できます。

<?php

$meterProvider = createMeterProvider();
Sdk::builder()
    ->setMeterProvider($meterProvider)
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

setAutoShutdown(true)を呼ぶことでプログラムの終了時にProviderのshutdownメソッドを実行することができます。
MeterProviderのshutdownメソッドでは終了処理を行なっており、収集したメトリクスを送信しています。 つまり、プログラムが終了するとメトリクスが送信されます。

メトリクスの記録

メトリクスを記録するには、まずMeterProviderからMeterを生成します。

<?php

$meter = Globals::meterProvider()->getMeter('php_meter_demo');

次に、MeterはInstrumentと呼ばれるものを生成します。 Instrumentはメトリクスの記録を担う、言わば計測器です。
どのメトリクスタイプを使うかで生成するInstrumentが変わり、ここではcreateHistogram()を呼んでヒストグラム用のInstrumentを生成しています。もし、カウンターであればcreateCounter()を呼びます。

<?php

$histogram = $meter->createHistogram('request_latency', 's', 'Random number');

最後に、Instrumentを使って値を記録します。

<?php

$histogram->record($requestDuration);

ここまでのまとめ

OpenTelemetryをセットアップしてからメトリクスを記録するまでのコードを以下にまとめます。
sleep関数に$_GET['sleep']を渡すことで、サンプルコードのリクエストレイテンシをコントロールできるようにしています。

<?php

declare(strict_types=1);

use OpenTelemetry\API\Globals;
use OpenTelemetry\Contrib\Otlp\ContentTypes;
use OpenTelemetry\Contrib\Otlp\MetricExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Metrics\Aggregation\ExplicitBucketHistogramAggregation;
use OpenTelemetry\SDK\Metrics\Data\Temporality;
use OpenTelemetry\SDK\Metrics\Exemplar\ExemplarFilter\WithSampledTraceExemplarFilter;
use OpenTelemetry\SDK\Metrics\Instrument;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MeterProviderInterface;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Metrics\StalenessHandler\NoopStalenessHandlerFactory;
use OpenTelemetry\SDK\Metrics\View\CriteriaViewRegistry;
use OpenTelemetry\SDK\Metrics\View\SelectionCriteriaInterface;
use OpenTelemetry\SDK\Metrics\View\ViewTemplate;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SemConv\ResourceAttributes;

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

$startTimeSec = microtime(true);

$meterProvider = createMeterProvider();
Sdk::builder()
    ->setMeterProvider($meterProvider)
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

// リクエスト処理にかかる時間を指定
sleep((int)$_GET['sleep']);

$meter = Globals::meterProvider()->getMeter('php_meter_demo');
$histogram = $meter->createHistogram('request_latency', 's', 'Random number');

$endTimeSec = microtime(true);
$requestDuration = $endTimeSec - $startTimeSec;
$histogram->record($requestDuration);


function createMeterProvider(): MeterProviderInterface
{
    $reader = new ExportingReader(
        new MetricExporter(
            PsrTransportFactory::discover()->create(
                'http://otelcol:4318/v1/metrics',
                ContentTypes::JSON
            ),
            Temporality::DELTA,
        )
    );
    $resource = ResourceInfo::create(
        Attributes::create(
            [
                ResourceAttributes::SERVICE_NAME => 'test-application',
                ResourceAttributes::SERVICE_VERSION => '1.0',
                'project_id' => 'sample-project',
                'location' => 'asia-northeast1',
                'cluster' => '1',
                'namespace' => 'abc',
                'job' => random_int(1, 100),
                'instance' => random_int(1, 100),

            ]
        )
    );
    $views = (new CriteriaViewRegistry());
    $views->register(
        // 条件文をスキップ
        new class implements SelectionCriteriaInterface {
            public function accepts(
                Instrument $instrument,
                InstrumentationScopeInterface $instrumentationScope
            ): bool {
                return true;
            }
        },
        // バケット定義を設定
        ViewTemplate::create()
            ->withName('request_latency')
            ->withAggregation(new ExplicitBucketHistogramAggregation([0, 1, 2, 3, 4])),
    );

    return new MeterProvider(
        null,
        $resource,
        ClockFactory::getDefault(),
        Attributes::factory(),
        new InstrumentationScopeFactory(Attributes::factory()),
        [$reader],
        $views,
        new WithSampledTraceExemplarFilter(),
        new NoopStalenessHandlerFactory(),
    );
}

OpenTelemetry Collector の構築

PHPでは収集したメトリクスをGoogle Cloud MonitoringやGoogle Managed Prometheus に直接送信するためのExporterが記事執筆時点では提供されていません。 そのため、OpenTelemetry Collector(以下 Collector)を介して送信します。

Collectorの構築方法はTraceを取得する時に実施した方法と同様のため、今回使用するCollectorの設定ファイルのみ説明します。

receivers:
  otlp:
    protocols:
      http:

exporters:
  googlemanagedprometheus:

service:
  pipelines:
    metrics:
      receivers: [otlp]
      exporters: [googlemanagedprometheus]

receiversはTraceの時と同じく、OTLP形式のデータをHTTP経由で取り込みます。
exportersには、Google Managed Prometheusに送信するように設定します。
serviceのpipelinesにmetricsを追加し、receiversとexportersで定義したものを指定します。

出力結果の確認

構築したCollectorコンテナを立ち上げ、http://localhost:9010/metrics.phpを叩くと作成したPHPファイルが実行されるように準備します。

そして、ベンチマークツールのheyを使って以下スクリプトを実行します。これで各バケットのカウントが予め想定した値になるようにリクエストを発行します。-cは同時実行数、-nはリクエスト数を表しています。

#!/bin/sh

# バケット[0,1)にカウントされるリクエストを3件発行
hey -c 3 -n 3 http://localhost:9010/metrics.php?sleep=0

# バケット[1,2)にカウントされるリクエストを20件発行
hey -c 5 -n 20 http://localhost:9010/metrics.php?sleep=1

# バケット[2,3)にカウントされるリクエストを3件発行
hey -c 3 -n 3 http://localhost:9010/metrics.php?sleep=2

# バケット[3,4)にカウントされるリクエストを40件発行
hey -c 5 -n 40 http://localhost:9010/metrics.php?sleep=3

このスクリプトを1分間隔で実行すると、Cloud Monitoringに収集したメトリクスが表示され、ヒートマップ形式で確認できます。

スクリーンショット 2024-01-04 17.23.05.png (136.5 kB)

ヒートマップの見方は上述したものと同じく、色が明るいほど高いバケットカウントを示します。 最もリクエスト数が多いバケット[3,4)が白色、その次に多い[1,2)が明るい紫色、最も少ない[0,1)と[2,3)が暗い紫色で表現されています。

まとめ

PHPでOpenTelmetryを使ってメトリクスを収集し、Cloud Monitoring上で可視化する方法を見てきました。このようにメトリクスを活用して、信頼性を担保していきたいですね。

ホワイトプラスでは、ビジョンバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など「生活領域×テクノロジー」で事業を展開している会社です。どんな会社か気になった方はオウンドメディア「ホワプラSTYLE」をぜひご覧ください。
オンラインでカジュアル面談もできますので、ぜひお気軽にお問い合わせください。

採用情報 | 株式会社ホワイトプラス