Unengineered Weblog

PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND

OpenTelemetry のヒストグラムメトリックを計装して Mackerel に投稿する

Mackerel Advent Calendar 2023 12月17日の記事です。 qiita.com

Mackerel は OpenTelemetry のメトリックに対応するよう開発中です。この記事では OpenTelemetry のヒストグラムメトリックの詳細と Mackerel での扱い、 SDK を用いてヒストグラムを計装する方法を解説します。

Mackerel の OpenTelemetry のメトリック対応

mackerel.io

クローズドベータテスト中の Mackerel の OpenTelemetry 対応では最近ヒストグラムメトリックに対応しました。OpenTelemetry の仕様では

  • gauge
  • sum
  • histogram
  • exponential histogram

の4種類のメトリックが定められています(summary はすでに legacy 扱いのため省略しています)。

opentelemetry.io

今回の対応で Mackerel は上記4種のメトリック全てに対応しました。 gauge と sum は集計方法に違いはあるものの、データの形式としてはどちらもいわゆる「一般的なメトリック」です。つまりある時点に対しての値(データポイント)が一つのスカラー値です。

それに対して histogram, exponential histogram のデータポイントは度数分布となっています。度数分布は中学や高校の数学で習ったとおり次のような形式です。

階級 [ms] 度数
(0, 5] 0
(5, 10] 1
(10, 25] 2
(25, 50] 5
(50, 75] 3

histogram と exponential histogram のデータポイントには上記の度数分布とともにデータの数 (count)、合計 (sum)、最大値(max)、最小値(min) といった情報も含まれています。

ヒストグラムメトリックが最も使われているのはおそらく HTTP のレスポンスタイムの計装ではないでしょうか。ウェブアプリケーションではレスポンスタイムのように一つのリクエストに対して計装される数値を記録するときにヒストグラムをつかうのが適しています。

histogram と exponential histogram の違い

OpenTelemetry の規格としての "histogram" は一般的に想像されるヒストグラムです。explicit bucket histogram ともいいます。メトリックを計装するライブラリは、計装した数値をある階級の度数として記録します。階級は各自定義するごとができます(デフォルトは [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ])。

この histogram には一つ問題点があります。階級を事前に定義しないといけないというところです。例えばレスポンスタイムが 10 ms ~ 100 ms 程度のアプリケーションをデフォルトの階級を使って計測すると (10, 25] (25, 50] (50, 75] (75, 100] の4つの階級に収まってしまいます。16個もある階級のうち4つしか使っておらず解像度が低いです。階級を適切に設定すれば良いのですが、コンポーネントが増え、記録されるヒストグラムメトリックの種類が増えると設定は大変です。

一方 exponential histogram はこの階級を定義する際の問題がありません。なぜなら階級が自動的に決まるからです。 メトリック計装ライブラリは記録された値をもとに scale を計算します。exponential histgoram の階級はこの scale を階級の index によって冪乗(exponentiation)した値と定義されています。例えば  \mathtt{scale} = sのとき、 \mathtt{index} = i の階級は

 [ (2^{2^{-s}})^i, (2^{2^{-s}})^{i+1} )

と表すことができます。計装ライブラリは記録された値にたいして解像度が高くなるように scale を計算します。詳しい仕組みは省略しますが、 exponential histogram は設定が不要で高解像度のヒストグラムが計装できるメトリックタイプだと捉えればいいでしょう。記事の後半では exponential histogram を使って計装する方法を説明しています。 ただし exponential histogram は比較的あたらしいメトリックタイプなためモニタリングツールによって対応がバラバラです。たとえば Prometheus ではこのブログを執筆した時点(2023/12/18)では まだ expoential histogram のドキュメントが整備されていません。

次の OpenTelemetry のブログには exponential histogram の長所がまとまっています。 opentelemetry.io

Exponential histogram の仕様は次のページで見ることができます。

opentelemetry.io

Mackerel に histogram を投稿する

ベータテストのMackerel では投稿されたヒストグラム (histogram, exponential histogram どちらでも)にたいして、いくつかの代表値が計算をして、その値を保存します。現在は count, sum, max, min p99 (99パーセンタイル), p95, p90 の最大7種のメトリックになります。「最大」というのはメトリック収集の設定によっては sum, max, min の情報がなかったりするからです。 これらのメトリックは [ヒストグラムメトリックの名前] . [suffix (sum や p90など)] の名前をもつメトリックとしてクエリすることができます。

レスポンスタイムを計装する

前回のブログでも登場した otelhttp をつかうとレスポンスタイムがヒストグラムとして計装されます。

rmatsuoka.hatenablog.com

使い方はかんたん。 パッケージ net/http の Handler を関数 otelhttp.NewHandler でラップするだけ。(もちろんメトリックの送信先を Meter Provider によって設定する必要がありますが、ここでは省きます)これだけでレスポンスタイムを計装することができます。

http.ListenAndServe(":8080", otelhttp.NewHandler(&mux, "server"))

Mackerel に投稿したヒストグラムメトリック(レスポンスタイム)をグラフで描画した図

手動で計装する

手動で計装するときは Meter からヒストグラムのカウンターを初期化することで計装できます。 Meter とはメトリックを記録するクラスで、Go では普通パッケージごとひとつだけグローバル変数 meter としてインスタンスを初期化することが多いです。

pkg.go.dev

私は package ごとに次のような meter.go を用意しました。カウンターはプログラム全体で使うためグローバルに定義をしました。 この例では整数 (int64) の値を記録する fooCounter を定義しています。

package foo

import (
    "reflect"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/metric"
)

var meter otel.Meter
var fooCounter metric.Int64Histogram

func init() {
    meter = otel.Meter(reflect.TypeOf(t{}).PkgPath())

    fooCounter, err = meter.Int64Histogram("foo metric's name")
    if err != nil {
        otel.Handle(err)
    }
}

数値を記録するときは次のように呼び出します

fooCounter.Record(ctx, int64(n))

exponential histogram を計装する

上記の設定のままでは explicit bucket histogram で計装されます。前述のとおり exponential histogram のほうが便利ですから、そちらに変更しましょう。 OpenTelemetry の metric を計装するライブラリでは View と呼ばれる仕組みが仕様として決められています。これを用いると計装の方法を override してカスタマイズすることができます。 view によって explicit bucket histogram ではなく exponential histogram として計装しましょう。

// histogram をすべて exponential histogram で記録する
view := metric.NewView(
    metric.Instrument{Kind: metric.InstrumentKindHistogram},
    // 160 と 20 はデフォルト値
    metric.Stream{Aggregation: metric.AggregationBase2ExponentialHistogram{MaxSize: 160, MaxScale: 20}},
)

mp := metric.NewMeterProvider(
    metric.WithReader(
        metric.NewPeriodicReader(meterExporter, metric.WithInterval(time.Minute)),
    ),
    metric.WithResource(resources),
    metric.WithView(view),
)