Carpe Diem

備忘録

Prometheus で独自メトリクスを用意する

概要

Metric types | Prometheus で定義されているようにPrometheusでは大きく4つのメトリクスタイプがあります。

  • Counter
  • Gauge
  • Summary
  • Histogram

今回はGoのPrometheusクライアントライブラリを用いて各メトリクスを独自で用意する方法を説明します。

環境

  • Prometheus 2.11.1
  • Golang 1.12.7
  • prometheus/client_golang 1.0.0

expose

まずはPrometheusが監視対象のメトリクスをPullできるよう/metricsのエンドポイントを用意します。
公式クライアントとしてpromhttpというライブラリが提供されているのでそれを使います。

package main

import (
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    http.Handle("/metrics", promhttp.Handler())
    log.Fatal(http.ListenAndServe(":8080", nil))
}

メトリクスタイプ

先に挙げた4つのメトリクスタイプを順に説明します。

GolangではCollectorというインタフェースを実装した形であらかじめ用意されています。
それを用いて具体的な実装を行います。

Counter

カウンタはイベントの数やサイズを追跡します。

など、常に増加する値に対して使用します。

具体的な実装

例としてHTTPのリクエスト数をカウントするコレクタを用意します。

以下のようなコードになります。それぞれ説明していきます。

var (
        httpReqs = prometheus.NewCounterVec(
                prometheus.CounterOpts{
                        Name: "http_requests_total",
                        Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
                },
                []string{"code", "method"},
        )
)

func init() {
        prometheus.MustRegister(httpReqs)
}

func main() {
        http.Handle("/metrics", promhttp.Handler())
        count()
        log.Fatal(http.ListenAndServe(":8080", nil))
}

func count() {
        httpReqs.WithLabelValues("404", "POST").Add(42)

        m := httpReqs.WithLabelValues("200", "GET")
        for i := 0; i < 1000000; i++ {
                m.Inc()
        }
}

コレクタの用意

Counterというコレクタを使います。

httpReqs := prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
    },
    []string{"code", "method"},
)

HTTP Status Codeやメソッドで分類できるよう、

  • code
  • method

の2つのラベルを用意します。

コレクタの登録

prometheus.MustRegisterで登録します。
同時に複数登録可能です。

prometheus.MustRegister(httpReqs)

実行

Counterには2つカウンティング用のメソッドが用意されており、

  • Inc(): 1ずつ増やす
  • Add(): 引数の値分増やす

を使います。
本来はミドルウェアとして用意してハンドラにリクエストが来たタイミングでカウントしていきますが、今回は簡単のため自分でカウントします。

httpReqs.WithLabelValues("404", "POST").Add(42)

m := httpReqs.WithLabelValues("200", "GET")
for i := 0; i < 1000000; i++ {
    m.Inc()
}

動作確認

実行すると

# HELP http_requests_total How many HTTP requests processed, partitioned by status code and HTTP method.
# TYPE http_requests_total counter
http_requests_total{code="200",method="GET"} 1e+06
http_requests_total{code="404",method="POST"} 42

このようにリクエスト数のカウンタが増えていきます。

Gauge

ゲージは何らかの状態のスナップショットです。

  • goroutineの数
  • キューに入っているアイテムの数
  • キャッシュのメモリ使用量
  • アクティブなスレッドの数

カウンタのような時系列データ的に加算されるものではなく、その時点での値が重要な要素です。

具体的な実装

例としてblobストレージのオペレーションキューのゲージを用意します。

以下のようなコードになります。それぞれ説明していきます。

var (
        opsQueued = prometheus.NewGauge(prometheus.GaugeOpts{
                Namespace: "our_company",
                Subsystem: "blob_storage",
                Name:      "ops_queued",
                Help:      "Number of blob storage operations waiting to be processed.",
        })
)

func init() {
        prometheus.MustRegister(opsQueued)

}

func main() {
        http.Handle("/metrics", promhttp.Handler())
        snapshot()
        log.Fatal(http.ListenAndServe(":8080", nil))
}

func snapshot() {
        opsQueued.Set(50)

        opsQueued.Add(10)

        opsQueued.Dec()
        opsQueued.Dec()
}

コレクタの用意

prometheus.Gaugeというコレクタを使います。

opsQueued := prometheus.NewGauge(prometheus.GaugeOpts{
    Namespace: "our_company",
    Subsystem: "blob_storage",
    Name:      "ops_queued",
    Help:      "Number of blob storage operations waiting to be processed.",
})

Namespaceをセットするとメトリクスの名前にprefixが付きます。今回でいうとour_company_blob_storage_ops_queuedという名前になります。

コレクタの登録

prometheus.MustRegister(opsQueued)

実行

prometheus.Gaugeには

  • Set(): 引数の値をセット(上書き)する
  • Inc(): インクリメント
  • Dec(): デクリメント
  • Add(): 引数を現在値に加算
  • Sub(): 引数を現在値から減算
  • SetToCurrentTime(): 現在時刻のセット

といったメソッドが用意されています。

いくつか適当に実行してみます。

opsQueued.Set(50)

opsQueued.Add(10)

opsQueued.Dec()
opsQueued.Dec()

動作確認

50 + 10 - 1 - 1 = 58の通りになりました。

# HELP our_company_blob_storage_ops_queued Number of blob storage operations waiting to be processed.
# TYPE our_company_blob_storage_ops_queued gauge
our_company_blob_storage_ops_queued 58

通常はn秒毎で常に値をセットしていくのですが、今回は1回のメソッドコールしかしてないので時系列データ的にはその時の値しかセットされません。

Summary

サマリは何らかのサイズの平均値を算出する際に使います。具体的には

  • レイテンシ
  • レスポンスサイズ

などです。

具体的な実装

例として池の温度の変化を追ってみます。

以下のようなコードになります。それぞれ説明していきます。

var (
        temps = prometheus.NewSummary(prometheus.SummaryOpts{
                Name: "pond_temperature_celsius",
                Help: "The temperature of the frog pond.",
        })
)

func init() {
        prometheus.MustRegister(temps)
}

func main() {
        http.Handle("/metrics", promhttp.Handler())
        go http.ListenAndServe(":8080", nil)
        simulate()
}

func simulate() {
        var i int
        for {
                i++
                time.Sleep(1 * time.Second)
                val := 30 + math.Floor(120*math.Sin(float64(i)*0.1))/10
                temps.Observe(val)
        }
}

コレクタの用意

Summaryというコレクタを使います。

temps := prometheus.NewSummary(prometheus.SummaryOpts{
    Name:       "pond_temperature_celsius",
    Help:       "The temperature of the frog pond.",
})

SummaryOptsにはObjectivesフィールドというランク評価設定がありますが、pythonクライアントではSummaryコレクタには用意されていないなど差異があるため今回は利用しません。

コレクタの登録

prometheus.MustRegister(temps)

実行

Summaryには

  • Observe()

というメソッドが用意されています。
引数にはイベントの何らかのサイズを渡します。値は非負数を入れてください。

var i int
for {
    i++
    time.Sleep(1 * time.Second)
    val := 30 + math.Floor(120*math.Sin(float64(i)*0.1))/10
    temps.Observe(val)
}

動作確認

マリコレクタを用意すると、

メトリクス名 説明
xxx_count observeが呼び出された回数
xxx_sum observeに渡された値の合計

の2つのメトリクスが用意されます。なので

rate(pond_temperature_celsius_sum[1m])/rate(pond_temperature_celsius_count[1m]) 

とすると、直近1分間の平均値が得られます。
Grafanaで描画すると以下のようになります。

ゲージと似てない?

サマリの値は一見ゲージと似てますが、ゲージは基本的にメモリ使用量などその時点の値を直接保持するのに対し、サマリはリクエストといった多数のデータをカウンティングして後で平均値を算出するために使用します。

Histogram

ヒストグラムはquantile(分位数)を知りたい時に利用します。
例えばレイテンシを

  • 50%ile
  • 90%ile
  • 95%ile

と分けることで、通常時のレイテンシや遅い時のレイテンシを推測できます。

具体的な実装

例として先程の池の温度のquantileを出してみます。

以下のようなコードになります。それぞれ説明していきます。

var (
        temps = prometheus.NewHistogram(prometheus.HistogramOpts{
                Name:    "pond_temperature_celsius",
                Help:    "The temperature of the frog pond.",
                Buckets: prometheus.LinearBuckets(20, 5, 5),
        })
)

func init() {
        prometheus.MustRegister(temps)
}

func main() {
        http.Handle("/metrics", promhttp.Handler())
        go http.ListenAndServe(":8080", nil)
        simulate()
}

func simulate() {
        var i int
        for {
                i++
                time.Sleep(1 * time.Second)
                val := 30 + math.Floor(120*math.Sin(float64(i)*0.1))/10
                fmt.Println(val)
                temps.Observe(val)
        }
}

サマリとかなり似ていますね。

コレクタの用意

Histogramというコレクタを使います。

temps := prometheus.NewHistogram(prometheus.HistogramOpts{
    Name:    "pond_temperature_celsius",
    Help:    "The temperature of the frog pond.",
    Buckets: prometheus.LinearBuckets(20, 5, 5),
})

ヒストグラムではバケットという集合で分類します。 LinearBuckets(start, width float64, count int)は線形のバケットを用意します。

今回でいうと20℃から始めて、範囲が5バケット5つ用意します。

コレクタの登録

prometheus.MustRegister(temps)

実行

サマリと同様にObserveを実行します。

var i int
for {
    i++
    time.Sleep(1 * time.Second)
    val := 30 + math.Floor(120*math.Sin(float64(i)*0.1))/10
    temps.Observe(val)
}

動作確認

ヒストグラムコレクタを用意すると、

メトリクス名 説明
xxx_count observeが呼び出された回数
xxx_sum observeに渡された値の合計
xxx_bucket{le="n"} n以下の値がobserveに登録された数

のメトリクスが用意されます。具体的には

# HELP pond_temperature_celsius The temperature of the frog pond.
# TYPE pond_temperature_celsius histogram
pond_temperature_celsius_bucket{le="20"} 12
pond_temperature_celsius_bucket{le="25"} 21
pond_temperature_celsius_bucket{le="30"} 25
pond_temperature_celsius_bucket{le="35"} 33
pond_temperature_celsius_bucket{le="40"} 45
pond_temperature_celsius_bucket{le="+Inf"} 56
pond_temperature_celsius_sum 1699.8999999999999
pond_temperature_celsius_count 56

このように表現されます。

pond_temperature_celsius_bucketを実行すると各バケットのカウントが増えていることが確認できます。

rate()

xxx_bucketの値はカウントなのでrate()で割合を出せます。

rate(pond_temperature_celsius_bucket[1m])

Grafanaで描画すると以下のようになります。

histogram_quantile()

quantileを求めたい時はhistogram_quantile関数を使います。例えば50%ileは

histogram_quantile(0.5, rate(pond_temperature_celsius_bucket[1m]))

で求まります。

注意点

ヒストグラムはカーディナリティが高い

バケットの10個あれば時系列データはsum, countをあわせて12個になります。
なのでデータ量が非常に多くなるため、しばしばPrometheusの負荷や容量を圧迫する原因になります。

ヒストグラムの数が少なければ問題ないですが、多い場合はサマリメトリクスへ切り替えを検討した方が良いです。

rateの範囲はなるべく狭く

通常ヒストグラムのrateでは5min, 10minといった範囲を使います。
ヒストグラムのデータは他のコレクタに比べて多いため、rateの範囲が長くなると処理しなければいけないサンプル数が非常に多くなってしまう可能性があるためです。

その他

promhttpにはデフォルトで2つのコレクターが備わっています。

なので

    http.Handle("/metrics", promhttp.Handler())
    log.Fatal(http.ListenAndServe(":8080", nil))

だけで以下のメトリクスが取得できます。

# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 4.363e-06
go_gc_duration_seconds{quantile="0.25"} 4.363e-06
go_gc_duration_seconds{quantile="0.5"} 1.1156e-05
go_gc_duration_seconds{quantile="0.75"} 2.0274e-05
go_gc_duration_seconds{quantile="1"} 2.0274e-05
go_gc_duration_seconds_sum 3.5793e-05
go_gc_duration_seconds_count 3
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 9
...
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1024
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 9
...
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 41
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

サンプルコード

今回使用したコードはこちらです。

github.com

ソース