おそらくはそれさえも平凡な日々

GoのアプリケーションをOpenMetricsを使って監視する

前のエントリでDatadogについて書いたが、実際にGoのアプリケーションがOpenMetricsを吐くようにするのはどうのようすれば良いかをもう少し解説します。

OpenMetricsとは?

元々[Prometheus]が利用しているフォーマット。Prometheusは"Promethues exporter"と呼ばれる監視対象からメトリクスを集約する作りになっている。

Prometheus exporterは実は「単なるHTTPのエンドポイント」であり、そのレスポンスが独自のテキストフォーマットになっている。このフォーマットを標準化しようとして提唱されているのがOpenMetrics。

https://openmetrics.io/

実際問題としては、Prometheusのドキュメントの方がまだまだ充実している。

DatadogにはOpenMetricsのインテグレーションがあり、自前でPrometheusサーバーを建てずともPrometheus exporterからのデータ収集をやってくれる。

実際に試してみる

百聞は一見にしかず、ということで、公式のガイドのGoアプリケーション実装サンプルを動かしてみる。

package main

import (
    "net/http"

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

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

これだけでれっきとした、Prometheus exporter機能を備えたGoのアプリケーションとなる。このコードを go run main.go すると2112ポートが開いてHTTPアクセスを受け付けるので、そこの/metricsにアクセスしてみたのが以下。

$ curl http://localhost:2112/metrics
...(snip)...
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 7.1649288e+07
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 8
# 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"} 0
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

go_memstats_sys_bytes 7.1649288e+07 のように、メトリクスキーと値がスペースで区切られているシンプルなテキストフォーマット。# で始まる行はコメントアノテーションでメトリクスの説明やデータタイプが記載されている。

メトリクスタイプのはここでは"gauge"と"counter"がある。gaugeはそのままの測定値で、counterはカウンター値である。他にも"summary"と"histogram"がある。ref. https://prometheus.io/docs/concepts/metric_types/

promhttp_metric_handler_requests_total{code="200"} 0 のように、メトリクスキーにラベルを付与できる点も特徴で、これによりmulti demensionalなメトリクス解析ができる。

メトリクスキーやラベルの命名についてはhttps://prometheus.io/docs/practices/naming/を見ると参考になる。

promhttpあれこれ

前項のサンプルの promhttp.Handler() をしたら、Goのアプリケーションの基本的なメトリクスは取れる。まずは最低限そこから始めても良いが、アプリケーション独自のメトリクスを取りたくなるのが人情であり、本命である。

promhttp.Handler() のレスポンスに独自のメトリクスを追加するには、github.com/prometheus/client_golang/prometheus/promautoパッケージを使うのがお手軽。例えば以下のような具合である。

// 宣言
var totalConnMetric = promauto.NewCounter(prometheus.CounterOpts{
    Name: "ws_conn_total",
    Help: "total connections",
})
// インクリメント
totalConnMetric.Inc()

メトリクスにラベルを付与したい場合は以下のようにすれば良い。

// 宣言
var totalConnMetric = promauto.NewCounterVec(prometheus.CounterOpts{
    Name: "ws_conn_total",
    Help: "total connections",
}, []string{"pod_id"})
// インクリメント
totalConnMetric.WithLabelValues("xxxx-pod-id-xxxx").Inc()
// 上と同様のインクリメント(ラベルが複数あるときは`With`の方が分かり良い)
totalConnMetric.With(prometheus.Labels{"pod_id":"xxxx-pod-id-xxxx"}).Inc()

(小ネタ)ECSのtask idを取得する

ECSでDatadog Agentに監視対象コンテナをdiscoveryさせている場合、その監視対象のtask idやcontainer id的なものをメトリクスに付与してくれない。これは少し困るのだが、上のように独自で"pod_id"ラベルを付与することでカバーしている。

pod idの取得は以下のようにアプリケーション起動前にラッパースクリプト内で、ECS TaskのARNを取得して環境変数にぶちこんでいる。

export MYAPP_POD_ID=$(wget -qO- -T 5 ${ECS_CONTAINER_METADATA_URI}/task | jq -r '.TaskARN' | cut -f 2 -d'/')
exec /bin/myapp

アプリケーションコンテナにはaplineを使っている。wgetは標準で入っているやつを使っているが、このようにリトライをサクッと設定できるのは便利。またjqapk add jqで入るので驚いた。

cutを使っているのは、Task Arnは "arn:aws:ecs:us-west-2:012345678910:task/2b88376d-aba3-4950-9ddf-bcb0f388a40c" のようなフォーマットなので、UUIDの部分だけを取り出したかったため。

コンテナからアプリケーションを起動する時に、Goのバイナリをダイレクトで起動したい気持ちはあるものの、このようにラッパーシェルを噛ましておくと、こういう雑なワンライナーを事前に呼び出すこととかができるので便利。

promhttp.Handler() を使わずに独自メトリクスのみを出力する

promhttp.Handler() は便利だが、標準で色々出力してくれるので、それがnoisyなこともある。そういう場合は自分でサラからハンドラーを作る。

この場合は、独自のprometheus.Registryを定義し、それにメトリクスを登録すし、そのRegistryを使ってハンドラーを作る。具体的には以下のような具合。

var (
    promReg         = prometheus.NewRegistry()
    totalConnMetric = promauto.NewCounter(prometheus.CounterOpts{
        Name: "ws_conn_total",
        Help: "total connections",
    }
)

func init() {
    promreg.MustRegister(totalConnMetric)
}

func main() {
    http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{}))
    http.ListenAndServe(":2112", nil)
}

前のエントリで紹介した、監視コンテナ"watchdog"はこの方法を使っている。

ちょっとした難点

ということでgithub.com/prometheus/client_golangは便利なのだが、依存が多いのが少し困りもの。特にOpenMetricsはこのエントリで説明したテキスト形式だけではなく、protobufでの出力も対応しているが、僕らの用途ではテキストだけでよいのに、protobuf関連の依存もアプリケーションに入ってしまうのが少しだるい。

テキストフォーマット出すだけなら、依存の少ないパッケージを独自で作ることも可能そうだが、そこまでやる気力は現状ないので、今はこれを使っている。公式ライブラリだし。

created at
last modified at

2020-05-18T02:02:21+0900

comments powered by Disqus