Edited at
Z LabDay 14

アプリケーションでPrometheusのメトリクスを直接サポートする

More than 1 year has passed since last update.


はじめに

Prometheusは、「プル型」のモニタリングシステムで、Prometheusサーバーから監視対象に対してHTTPでアクセスしてメトリクスを収集(scrape)します。

そのため、Prometheusで監視をするためには、監視される側でPrometheus形式のメトリクスを取得できるHTTPエンドポイントを公開しておく(instrument)必要があります。1

既に多くのソフトウェアで、このPrometheus形式のメトリクスを出力するためのExporterと呼ばれる外部プログラムが用意されています。

Exporter一覧

また、KubernetesやEtcd、SkyDNSなどは、アプリケーション自体で直接Prometheus形式のメトリクス出力をサポートしています。

Prometheus形式のメトリクスをサポートしているソフトウェア一覧

これらでカバーできないメトリクスに関しては、

1. 自分でExporterを作る

2. アプリケーション自体で直接Prometheusのメトリクス出力をサポートする

という選択肢が考えられます。

監視したいアプリケーションを直接修正できない(または、したくない)場合は、Exporterを作ることになります。

Exporterは、アプリケーションがPrometheusに依存しないので気軽に作れる反面、元々外部から取得できるメトリクスしか収集できないという制限もあります。

一方直接サポートする場合は、(もちろんアプリケーションを直接修正できる立場にあるという前提ですが、)アプリケーション内部のより細かいメトリクスを収集することができます。

今回はアプリケーション独自のメトリクスを収集することを想定して、アプリケーション自体で直接Prometheusのメトリクス出力をサポートする方法を検証してみました。

prometheus_instrumentation.002.png


Prometheusのクライアントライブラリ

Exporter/直接サポートに関わらず、Prometheus形式のメトリクスを出力する時は、Prometheusのクライアントライブラリ を利用するのが楽です。

このクライアントライブラリはPrometheusのメトリクスタイプを実装しているので、これを使うと簡単にPrometheus形式のメトリクスを作成することができます。

各言語で名前の違いはあると思いますが、メトリクス収集に使用するライブラリは大体以下のような構成になっています。



  • Collector ...各メトリクスを取得するインタフェース2



    • Counter ...Counterタイプのメトリクスを収集するためのCollector実装


    • Gauge ...Gaugeタイプのメトリクスを収集するためのCollector実装


    • Histogram ...Histogramタイプのメトリクスを収集するためのCollector実装


    • Summary ...Summaryタイプのメトリクスを収集するためのCollector実装




  • CollectorRegistry ...Collectorをとりまとめるクラス。スクレイプされる度に登録されたCollectorのコールバック(e.g., collect)を呼び出してメトリクスを収集・出力する


  1. 各メトリクスを収集するためのCollectorを用意して、CollectorRegistryに登録しておき、

  2. スクレイプされた時にCollectorRegistryに登録されているCollectorのコールバックが呼ばれてメトリクスが収集・出力される

というのが、基本的な流れです。


Goでの実装例

今回は、Goのクライアントライブラリを使用してみます。

以下のような簡単なWebサーバーを対象として、Prometheus形式のメトリクス出力をサポートしてみます。


httpserver.go

package main

import (
"fmt"
"net/http"
"log"
"math/rand"
"time"
)

func main() {
helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().UnixNano())
switch rand.Intn(4) {
case 0:
log.Println("Hello!", )
fmt.Fprint(w, "Hello!", )
case 1:
log.Println("Hi!", )
fmt.Fprint(w, "Hi!")
case 2:
log.Println("Hey!", )
fmt.Fprint(w, "Hey!")
case 3:
log.Println("Error!", )
fmt.Fprint(w, "Error!!")
}
})
http.Handle("/", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}


ソースコード(Gist)

アクセスするたびに、3種類の挨拶とエラーをランダムで返すだけのWebサーバーです。

何を返したか後で確認できるように以下のようなログ出力もします。

% go run httpserver.go

2017/12/07 17:29:23 Hello!
2017/12/07 17:29:31 Hey!
2017/12/07 17:29:33 Hi!
2017/12/07 17:29:34 Error!
2017/12/07 17:29:36 Hi!
2017/12/07 17:29:37 Hello!
2017/12/07 17:29:39 Hi!


Step1. エラーをカウントする

失敗回数は重要なメトリクスなので、まずはError!と出力された回数をCounter型で収集してみます。

参考: https://prometheus.io/docs/practices/instrumentation/#failures

以下のようにコードを追加します。

% git diff 14eb176 daa222e

diff --git a/httpserver.go b/httpserver.go
index a4b223e..fea3c6e 100644
--- a/httpserver.go
+++ b/httpserver.go
@@ -6,8 +6,22 @@ import (
"math/rand"
"net/http"
"time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
)

+var (
+ errorCount = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "greeting_error_count_total",
+ Help: "Counter of HTTP requests resulting in an error.",
+ })
+)
+
+func init() {
+ prometheus.MustRegister(errorCount)
+}
+
func main() {
helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().UnixNano())
@@ -23,9 +37,11 @@ func main() {
fmt.Fprint(w, "Hey!")
case 3:
log.Println("Error!")
+ errorCount.Inc()
fmt.Fprint(w, "Error!!")
}
})
http.Handle("/", helloHandler)
+ http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
}

ソースコード(Gist)

errorCountというCollectorを登録しておき、エラーのタイミングでerrorCount.Inc()を呼び出すだけです。

最後に/metricsというパスでメトリクスを公開しています。

起動してhttp://localhost:8080/metricsにアクセスすると、以下のようなPrometheus形式のメトリクスが取得できます。

# HELP go_gc_duration_seconds A summary of the GC invocation durations.

# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
...(省略)
# HELP http_error_count_total Counter of HTTP requests resulting in an error.
# TYPE http_error_count_total counter
greeting_error_count_total 1

GoのクライアントではGo関連のメトリクス(GCの時間や起動しているGoroutineの数など)がデフォルトで出力されるようです。

最後の行で今回設定したエラー回数もちゃんと出力されていることがわかります。

各メトリクスには、ラベルを設定することもできます。

例えば、SkyDNSの場合は、エラーに対してsystemcauseというラベルを付与しています。

https://github.com/skynetservices/skydns/blob/master/metrics/metrics.go#L82


Step2. ログをカウントする

次は、挨拶を返した回数をCounter型で収集してみます。

今回はラベルを設定することで、パターン別にグルーピングできるようにします。

参考: https://prometheus.io/docs/practices/instrumentation/#logging

% git diff daa222e 51d2d01

diff --git a/httpserver.go b/httpserver.go
index fea3c6e..74a90fb 100644
--- a/httpserver.go
+++ b/httpserver.go
@@ -16,10 +16,15 @@ var (
Name: "greeting_error_count_total",
Help: "Counter of HTTP requests resulting in an error.",
})
+ successCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Name: "greeting_success_count_total",
+ Help: "Counter of HTTP requests resulting in a success.",
+ }, []string{"type"})
)

func init() {
prometheus.MustRegister(errorCount)
+ prometheus.MustRegister(successCount)
}

func main() {
@@ -28,12 +33,15 @@ func main() {
switch rand.Intn(4) {
case 0:
log.Println("Hello!")
+ successCount.WithLabelValues("hello").Inc()
fmt.Fprint(w, "Hello!")
case 1:
log.Println("Hi!")
+ successCount.WithLabelValues("hi").Inc()
fmt.Fprint(w, "Hi!")
case 2:
log.Println("Hey!")
+ successCount.WithLabelValues("hey").Inc()
fmt.Fprint(w, "Hey!")
case 3:
log.Println("Error!")

ソースコード(Gist)

typeというラベルをもったsuccessCountというCollectorを用意して、インクリメント時にラベルを指定します。

以下のように、ラベル付きでメトリクスが取得できました。

# HELP greeting_success_count_total Counter of HTTP requests resulting in an error.

# TYPE greeting_success_count_total counter
greeting_success_count_total{type="hello"} 3
greeting_success_count_total{type="hey"} 1
greeting_success_count_total{type="hi"} 1


Step3. HTTP関連のメトリクスを収集する

最後にGoのpromhttpパッケージを使用したメトリクス収集を紹介します。

Goのpromhttpパッケージには、InstrumentHandlerXというfunctionが用意されています。

参考: https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp

これはメトリクス収集用のCollectorとnet/httpのhandlerを渡すと、handlerをメトリクスを収集できる状態にラップして返してくれるという便利なfunctionです。

リクエスト数、リクエスト時間、レスポンスサイズ等のHTTP関連のメトリクスを簡単に収集することができます。

以下のようにコードを追加します。

% git diff 51d2d01 c4756da

diff --git a/httpserver.go b/httpserver.go
index 74a90fb..5d46163 100644
--- a/httpserver.go
+++ b/httpserver.go
@@ -20,11 +20,28 @@ var (
Name: "greeting_success_count_total",
Help: "Counter of HTTP requests resulting in a success.",
}, []string{"type"})
+ requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Name: "http_request_count_total",
+ Help: "Counter of HTTP requests made.",
+ }, []string{"code", "method"})
+ requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Name: "http_request_duration_seconds",
+ Help: "A histogram of latencies for requests.",
+ Buckets: append([]float64{0.000001, 0.001, 0.003}, prometheus.DefBuckets...),
+ }, []string{"code", "method"})
+ responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Name: "http_response_size_bytes",
+ Help: "A histogram of response sizes for requests.",
+ Buckets: []float64{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20},
+ }, []string{"code", "method"})
)

func init() {
prometheus.MustRegister(errorCount)
prometheus.MustRegister(successCount)
+ prometheus.MustRegister(requestCount)
+ prometheus.MustRegister(requestDuration)
+ prometheus.MustRegister(responseSize)
}

func main() {
@@ -49,7 +66,13 @@ func main() {
fmt.Fprint(w, "Error!!")
}
})
- http.Handle("/", helloHandler)
+ // Instrument helloHandler
+ wrappedHelloHandler := promhttp.InstrumentHandlerCounter(requestCount,
+ promhttp.InstrumentHandlerDuration(requestDuration,
+ promhttp.InstrumentHandlerResponseSize(responseSize, helloHandler),
+ ),
+ )
+ http.Handle("/", wrappedHelloHandler)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
}

ソースコード(Gist)

リクエスト数、リクエスト時間、レスポンスサイズを収集するCollectorを用意しています。

最後にInstrumentXでhandlerをラップするだけで、以下のように対応するメトリクスを収集してくれます。

# HELP http_request_count_total Counter of HTTP requests made.

# TYPE http_request_count_total counter
http_request_count_total{code="200",method="get"} 15
# HELP http_request_duration_seconds A histogram of latencies for requests.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{code="200",method="get",le="1e-06"} 0
http_request_duration_seconds_bucket{code="200",method="get",le="0.001"} 15
http_request_duration_seconds_bucket{code="200",method="get",le="0.003"} 15
...(省略)
http_request_duration_seconds_bucket{code="200",method="get",le="5"} 15
http_request_duration_seconds_bucket{code="200",method="get",le="10"} 15
http_request_duration_seconds_bucket{code="200",method="get",le="+Inf"} 15
http_request_duration_seconds_sum{code="200",method="get"} 0.0008033
http_request_duration_seconds_count{code="200",method="get"} 15
# HELP http_response_size_bytes A histogram of response sizes for requests.
# TYPE http_response_size_bytes histogram
http_response_size_bytes_bucket{code="200",method="get",le="0"} 0
http_response_size_bytes_bucket{code="200",method="get",le="2"} 0
http_response_size_bytes_bucket{code="200",method="get",le="4"} 7
http_response_size_bytes_bucket{code="200",method="get",le="6"} 11
http_response_size_bytes_bucket{code="200",method="get",le="8"} 15
...(省略)
http_response_size_bytes_bucket{code="200",method="get",le="20"} 15
http_response_size_bytes_bucket{code="200",method="get",le="+Inf"} 15
http_response_size_bytes_sum{code="200",method="get"} 76
http_response_size_bytes_count{code="200",method="get"} 15


おわりに

Prometheusでアプリケーション独自のメトリクスを収集するために、今回はアプリケーションで直接Prometheusのメトリクス出力をサポートする方法を検証してみました。

Prometheusのクライアントライブラリを使うことで、外部に公開されていないシステム固有のメトリクス出力を簡単にサポートすることができます。

アプリケーション自体を修正するので採用できる状況は限られると思いますが、ホワイトボックスモニタリングという観点では非常に有効だと思います。

KubernetesやPrometheusの利用を前提としているクラウドネイティブなアプリケーションでは、Prometheus形式のメトリクス出力を直接サポートしてみるのもアリではないでしょうか。


参考





  1. このようにHTTPエンドポイントを用意して計測することを"instrument"、Prometheusから監視対象にデータを取得することを"scrape"という単語で表現することが多いようです。 



  2. Collectorは独自に実装することも可能です。実装をいくつかみたところ、Exporterでは対象システムのメトリクスを一括で取得するCollectorを作っているケースが多く、直接サポートではあらかじめ用意されたタイプごとのCollectorを利用している場合が多いようです。