概要
Metric types | Prometheus で定義されているようにPrometheusでは大きく4つのメトリクスタイプがあります。
- Counter
- Gauge
- Summary
- Histogram
今回はGoのPrometheusクライアントライブラリを用いて各メトリクスを独自で用意する方法を説明します。
環境
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)
実行
- 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つのコレクターが備わっています。
- Processコレクタ
- CPU、メモリ、ファイルディスクリプタなど
- Goコレクタ
- GC、goroutine数、OSのスレッド数といったGoのランタイム情報
なので
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
サンプルコード
今回使用したコードはこちらです。