独自メトリクス取るなら Textfile Collector 独自メトリクス取るなら Textfile Collector

こんにちは、 sugitak です。今年は省エネモードで活動中なので、普通の1エントリになります。

# 前置き

Prometheus の監視対象は Exporter と呼ばれています。 Exporter は `/metrics` にて Prometheus 形式でデータを公開するようになっています。そして Exporter は各種ライブラリを使って **簡単に** 実装できます。…って。
いうじゃない？

### Golang のライブラリを使った例

```go
// A minimal example of how to include Prometheus instrumentation.
package main

import (
	"flag"
	"log"
	"net/http"

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

var addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")

func main() {
	flag.Parse()
	http.Handle("/metrics", promhttp.Handler())
	log.Fatal(http.ListenAndServe(*addr, nil))
}
```
https://github.com/prometheus/client_golang の `examples/simple/main.go` より。

### Python のライブラリを使った例

```py3
from prometheus_client import start_http_server, Summary
import random
import time

# Create a metric to track time spent and requests made.
REQUEST_TIME = Summary('request_processing_seconds', 'Time spent processing request')

# Decorate function with metric.
@REQUEST_TIME.time()
def process_request(t):
    """A dummy function that takes some time."""
    time.sleep(t)

if __name__ == '__main__':
    # Start up the server to expose the metrics.
    start_http_server(8000)
    # Generate some requests.
    while True:
        process_request(random.random())
```
https://github.com/prometheus/client_python の README.md より。

いうほど簡単じゃないですから！
**残念！！！**

### なんで大変なのかなぁ

なんとなく、監視用のスクリプトってそんなに難しくないものをイメージしている気がするんですよね。
Nagios なら返り値を 0, 1, 2 のどれにするかだけだったし、 Cacti 向けの SNMP も特定のファイルにデータを書くだけだし。Zabbix 向けのスクリプトだって数値を出力することが最終目標だし。

つまり監視を考えるとき、我々はなんとなく、「実行したら終わり」のスクリプトをイメージしてるんじゃないかと。

でも、 Exporter を作ろうとすると、必ずそれはサーバを作ることになる。サーバを作るとなると、まあ単純なスクリプトと比べればまともなオブジェクト指向を使うことになるし、ライブラリの方で隠蔽してくれていることで目に見えない世界が広がってしまう。

つらいなら、楽な手法を利用しましょ。

# ラクな方法

Prometheus を使う以上は node_exporter は動かしていると思うのだけど、そのオプションの中にあるわけですよ。 `--collector.textfile.directory` というものが。
このオプションでディレクトリを指定すると、 node_exporter はそのディレクトリ直下の `*.prom` なファイルを見て、その中身を node_exporter で出力してくれる。管理対象があんまり増えないからすごく楽！便利！犬！！

https://github.com/prometheus/node_exporter/

この機能、 `Textfile Collector` と呼ばれているので、他の資料を見るときはなんとなく覚えておいてください。
あと用語といえば、 `node_exporter` にアクセスしたときに大量のメトリクスが表示されますが、あの形式の名前は [text format](https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details) と呼ぶそうです。私はあれ Prometheus 形式とか呼んでたけど間違いでした。かなしー

## リクオ、 text format とはなんぞや？

さて、先ほどの Textfile collector の中身、実は text format なわけです。
思い出すために、ちょっと node_exporter の出力から抜き出してみましょう：

```
# 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
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 14
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.9.2"} 1
```

こんなやつを text format って呼んでたわけですね。

つらいのは、これこのまんま全部、コメント行っぽく見えているものも全て含めて "text format" だということ。つまり、単に `go_gc_duration_seconds_sum 0` とだけ出力しただけではいけない。
したがって、ライブラリを使いましょう、ということになります。

## textfile collector 向けのスクリプトを書く

https://github.com/prometheus/client_python を使って、簡単に textfile collector 向けスクリプトを書いていきましょう！

骨組みはこんな感じです。さっきと比べると圧倒的に簡単！

```py3
#!/usr/bin/env python3
import time
from prometheus_client import write_to_textfile, REGISTRY

TMP_PATH = '/path/to/textfile/directory/hogefuga.prom'

while True:
    write_to_textfile(TMP_PATH, REGISTRY)
    time.sleep(10)
```

`write_to_textfile` というのがミソで、このライブラリに溜め込まれたデータをすべて、 text format にてファイルへと書き込んでくれます。 REGISTRY はいわゆるおまじない（デフォルト引数にしてほしい…PR作ろう…）
ちなみにこのファイルは実ファイルのパスである必要があるんですよね…。標準入出力とか `/dev` 以下とかは使えませんよと。ただ、その代わり atomic な書き込みをサポートしてくれているので、書き換え中の動作について心配する必要がありません。安心！

### Gauge（ゲージ）を追加

Gauge は、好きな値を入れるやつです。温度とか、メモリ使用量とか、1分あたりアクセス数とか、そういうものですね。一番よく使うんじゃないかと。
雑にランダム値を入れる例：

```py3
#!/usr/bin/env python3
import random
import time
from prometheus_client import Gauge, write_to_textfile, REGISTRY

TMP_PATH = '/path/to/textfile/directory/hogefuga.prom'

g = Gauge('fuga_requests', 'Gauge')
while True:
    g.set(random.randint(1, 100))

    write_to_textfile(TMP_PATH, REGISTRY)
    time.sleep(10)
```

Gauge オブジェクトを作って、値をセットしているだけの例です。実用的なことをする際も、結局はこのメインループ内で外部 API などを叩くだけですね。イージー。
`Gauge()` の第一引数にはメトリック名を指定しています。第二引数はヘルプ文字列で、これは空でも大丈夫です。

### Counter（カウンタ）を追加

Counter は、そのままカウンタです。 ifconfig すると出てくるパケット受信数とかエラー数とか、ああいう「一度上がったあとに下がることはない（ただし restart 時を除く）」やつらのことです。

```py3
#!/usr/bin/env python3
import time
from prometheus_client import Counter, write_to_textfile, REGISTRY

TMP_PATH = '/path/to/textfile/directory/hogefuga.prom'

c = Counter('piyo_count', 'piyopiyo hiyoko san')
c.inc(100) # 初期値

while True:
    c.inc()

    write_to_textfile(TMP_PATH, REGISTRY)
    time.sleep(10)
```

基本的には Counter は `inc()` メソッドしか使えません。つまり減少がなく、増加しかできない。
値をセットすることもできないのですが、スクリプトを開始させたときに初期値を与えたかったら、たんに `inc(<初期値>)` と初期値を与えれば良いので、あまり困ることはないかと思われます。もし困る場合、 counter ではなく gauge を使うことを検討しても良いのではないでしょうか。

## ざっくり完成

さて、そんなこんなで、 Counter も Gauge も使った Example スクリプトはこんな感じになりました。

```py3
#!/usr/bin/env python3
import random
import time
from prometheus_client import Counter, Gauge, write_to_textfile, REGISTRY

TMP_PATH = '/path/to/textfile/directory/hogefuga.prom'

if __name__ == '__main__':
    c = Counter('piyo_count', 'piyopiyo hiyoko san')
    g = Gauge('fuga_requests', 'Gauge')
    while True:
        c.inc()
        g.set(random.randint(1, 100))

        write_to_textfile(TMP_PATH, REGISTRY)
        time.sleep(10)
```

たかだか20行くらいで済んでますね。やってることもわりと明瞭です。あとはこのスクリプトを supervisord 等に突っ込めば、以後 node_exporter がこの内容を出力してくれるようになります。いいっすねー。

```
$ curl localhost:9100/metrics 2>/dev/null
# HELP fuga_requests Gauge
# TYPE fuga_requests gauge
fuga_requests 100
# 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.1397e-05
go_gc_duration_seconds{quantile="0.25"} 4.1397e-05
go_gc_duration_seconds{quantile="0.5"} 4.3575e-05
go_gc_duration_seconds{quantile="0.75"} 4.3575e-05
go_gc_duration_seconds{quantile="1"} 4.3575e-05
（略）
# HELP node_time System time in seconds since epoch (1970).
# TYPE node_time gauge
node_time 1.5138587445742888e+09
# HELP piyo_count piyopiyo hiyoko san
# TYPE piyo_count counter
piyo_count 86
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="2",minor="7",patchlevel="10",version="2.7.10"} 1
```

実際に node_exporter の出力を見るとこんな感じ。
あれ、最後の行を見るに、どうやら私はうっかり python3 ではなく python2 で動かしていたようです(๑˃̵ᴗ˂̵)

# まとめ

Prometheus で独自メトリクスを取ることを考えたとき、独自 Exporter を書くのは頭を慣らす必要があって地味につらい。従来の監視のように簡単なスクリプトでもって監視項目を増やしたいですよね。
やはりここは node_exporter の Textfile Collector でしょうということで、その使い方とスクリプトの書き方ベーシックをバーっとご紹介しました。

これで Textfile Collector を利用した独自メトリクスは簡単に取れるようになるわけですが、ただやはり独自実装を増やしてしまうということには変わりないんですよね…。監視ツールのポータビリティを重視する人はおそらくこれもやりたくなくて、 statsd のような汎用的な仕組みを間に置きたいんじゃないかな。

今回はそこまでできませんが、まあ、独自メトリクスの取得方法としては、これはこれで一つのゴールではないでしょうか。

今年のアドベントカレンダー、埋まり具合を見ると半分ちょっとくらいなんですが、いろんな方面の質の高い記事があって最高ですね。遅れてしまったことを申し訳なく思いつつ、参加できたことが光栄でございます。

さてそれではみなさま、良い冬をお過ごしください。 Have a happy Prometheus life！!