Carpe Diem

備忘録

Prometheus でカスタムコレクタを用意する

概要

以前↓の独自メトリクスを作る方法を紹介しました。

christina04.hatenablog.com

これはdirect instrumentation(直接計装)というやり方で、アプリケーションサーバ自体がstatsを持ちprometheusにデータを渡すやり方です。

今回はそうではなく別プロセスや別サーバ、ホストOSのstatsを取り込んでコレクタとして登録する方法を紹介します。

イメージ的には各stats情報をprometheusフォーマットに変換するproxyを作る感じです。

環境

  • prometheus/client_golang 1.5.0

Collectorインタフェース

GoではCollector interfaceを実装することでCustom Collectorを作れます。

Collector interfaceは以下の2つのメソッドを持っています。

  • Describe(chan<- *Desc)
  • Collect(chan<- Metric)

Describeメソッド

Describeメソッドは自分が生成するメトリクスを説明する記述を返します。
具体的には、メトリクスの名前・ラベル名・ヘルプ文字列です。
Describeは登録時に呼び出され、メトリクスの重複登録を避けるために使われます。

Collectメソッド

Collectメソッドはターゲットのアプリケーションインスタンスから必要なデータを取り出し、クライアントライブラリにメトリクスを送り返します。
CollectはPrometheusサーバがスクレイピングする際に呼び出されます。

実装

それでは具体的な実装をしていきます。今回は

  • goroutineの数
  • threadの数

をゲージとして取得できるコレクタを用意します。

collectorの構造体

計測したい値を*prometheus.Desc型のフィールドで用意します。
prometheus.NewDesc()で生成可能です。

type collector struct {
        goroutinesDesc *prometheus.Desc
        threadsDesc    *prometheus.Desc
}

func newCollector() *collector {
        return &collector{
                goroutinesDesc: prometheus.NewDesc(
                        "goroutines",
                        "Number of goroutines that currently exist.",
                        nil, nil),
                threadsDesc: prometheus.NewDesc(
                        "threads",
                        "Number of OS threads created.",
                        nil, nil),
        }
}

Collectorインタフェースの実装

Describeの方は簡単で、フィールドの値をchannelにただ渡すだけです。

func (c *collector) Describe(ch chan<- *prometheus.Desc) {
        ch <- c.goroutinesDesc
        ch <- c.threadsDesc
}

Collect()の方ではメトリクスのためのデータを揃え、時には加工してフィールドに入れます。
prometheus.MustNewConstMetric()を使って渡します。

func (c *collector) Collect(ch chan<- prometheus.Metric) {
        ch <- prometheus.MustNewConstMetric(c.goroutinesDesc, prometheus.GaugeValue, float64(runtime.NumGoroutine()))
        n, _ := runtime.ThreadCreateProfile(nil)
        ch <- prometheus.MustNewConstMetric(c.threadsDesc, prometheus.GaugeValue, float64(n))
}

引数に入れる値のタイプとしては以下の3つがあります。

タイプ いつ使うか
GaugeValue ゲージの時
CounterValue カウンタの時
UntypedValue カウンタかゲージかがはっきりしない時

expose

Prometheusが監視対象のメトリクスをPullできるよう/metricsのエンドポイントを用意します。

func main() {
        reg := prometheus.NewRegistry()
        reg.MustRegister(newCollector())

        http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
        log.Fatal(http.ListenAndServe(":8080", nil))
}

動作検証

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

direct instrumentationかカスタムコレクタか

direct instrumentationかカスタムコレクタか悩む時があります。
例えばグローバル変数など内部statsを用意して、外部statsやイベントのデータを内部statsに反映、それをカスタムコレクタとして公開することができます。
通常このアプローチはdirect instrumentationですが、上記のようにカスタムコレクタとしても実現できます。

しかし基本的に内部で値を保持するものに関してはdirect instrumentationが良いです。

というのもグローバル変数など内部で状態を持つ場合にカスタムコレクタを使うと、write(値の更新)とread(スクレイピング)を自前で実装するため、race conditionが発生しないようatomic packageを使うなど考慮が必要です。

direct instrumentationはクライアントライブラリで提供されているので、内部で値を持ちますが↑のような課題はライブラリ側でよろしくやってくれるので意識する必要がありません。

ref: Writing exporters | Prometheus

サンプルコード

今回のサンプルコードはこちらです。

github.com

まとめ

Prometheusでカスタムコレクタを作る方法を説明しました。

参考