PrometheusのExporterをPythonで作る。Client Library無しで

Prometheusでのモニタリングのアーキテクチャというのは凡そ以下のとおりである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/7fa37094-a9e9-cb36-e62b-be0c18c70cca.png)
図の左から、
- Exporterは監視対象をモニターし、問い合わせを受ければ監視対象の現在のモニター値を返答する。
- Prometheusは複数あるExporterからモニター情報を収集し、自身のデータベースに蓄積する。
- GrafanaはPrometheusから一定期間のモニター情報を抽出し、ユーザーにグラフ表示する。
- Alert ManagerはPrometheusのあるモニター値が閾値を超えた場合に、ユーザーにアラート通知する。

実用的にはGrafanaは冗長化したPrometheusから重複排除したデータを抽出するThanos Querierを介したりするのだが、割愛する。

何の話がしたいかというと、PrometheusのExporterを自作したい。Prometheusは、例えばサーバーのCPU使用率とかを収集するNode Exporter等、めぼしい対象毎のExporterというのは存在する[(参考)](https://prometheus.io/docs/instrumenting/exporters/)のだが、めぼしくもない対象のExporterというのは既存では無いので、自分で作る必要がある。

各プログラミング言語に合わせたPrometheusのClient Libraryを使って、そのフレームワークを使うことも出来るのだが、このClient Libraryを使うというのが若干難である場合がある。

一つには、例えばPythonのClient Libraryを使うとして、Client Libraryに10個くらい標準でついてくるpython_gc_collections_totalみたいなメトリクスがウザい。Prometheusのディスク領域を使うし、こいつを無視するというのを読む側で認識しなければいけないのが手間である。

もう一つには、Client Libraryのインストールをpipから行う必要がある点で、インフラ現場ではサポートのないOSSを導入するのが嫌われる傾向が強く、例えば（サポートのある）RHELの一部として使えるpythonで出来る以上のことはしたくないといった事情がある。それを押して使うには最悪「PrometheusのClient Library上等ですよ。使うのやめるまで僕が無償でサポートし続けますよ」と宣言する必要があるのだが、しかしせいぜい数十行のスクリプトを維持するのとは違って、ファイルだけで100個以上あるClient Libraryを個人が無償で何年もサポートし続けるなど、どだい無理な話だ。
https://github.com/prometheus/client_python

PrometheusのExporterは、要はリクエストを受け付けたらメトリクス情報の一覧を返すHTTPサーバーである。
データとしてはHTMLを返すわけではなく、以下PrometheusのEXPOSITON FORMATSに従うものとなる。
Prometheus - EXPOSITION FORMATS
https://prometheus.io/docs/instrumenting/exposition_formats

例えばRHEL8同梱のPython 3.6で、Python標準ライブラリのhttp.serverを使ってPrometheus Exporterを作れると、思いがけない用途で有用だったりするのである。
https://docs.python.org/ja/3.6/library/http.server.html


# 環境
クラウドでもいいし、ローカルでも良いのでRHEL8の仮想マシン一つ。
Amazon LightsailでRHEL8が使えれば良いのだが、EC2でしか使えないので、今のところAzureで用意するのが一番楽。サイズはB1lsで十分。

# Exporterを作る
メトリクスの例として「/tmp」ディレクトリにあるファイルの個数を返すExporterを作ってみる。
要は、先のEXPOSIOTION FORMATSに倣う出力をするHTTPサーバーを作ればいいのだが、HTTPリクエストを捌く処理と、監視対象からメトリクスを収集する処理はスレッドを分ける方が都合良いので以下のような感じになる。

```python:tmp-exporter.py
import http.server
import socketserver
import threading
import time
import os

metrics = {}
flag = False

class MyHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
    global metrics
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()

        self.wfile.write(b"# HELP tmp_files The number of files in /tmp.\n")
        self.wfile.write(b"# TYPE tmp_files gauge\n")
        v = str(metrics["tmp_files"]).encode()
        self.wfile.write(b'tmp_files ' + v + b"\n")

def update():
    global metrics
    global flag
    while True:
        sleep_to = time.time() + 5.0

        ld = os.listdir("/tmp/")
        metrics["tmp_files"] = len(ld)

        while time.time() < sleep_to:
            if flag:
                return
            time.sleep(1.0)

if __name__ == "__main__":
    print("starting server")
    t = threading.Thread(target=update)
    t.start()
    Handler = MyHTTPRequestHandler
    with socketserver.TCPServer(("localhost", 8000), Handler) as httpd:
        print("serving at http://localhost:8000")
        httpd.serve_forever()
    flag = True
```

細かく言えばdo_GETで「/metrics」のパスにアクセスが有った場合に限り応答するのがより適切だがまあ、上記で動作する。
上記のスクリプトはMyHTTPRequestHandlerがSIGTERMを処理して終了してくれるので、そのままsystemdのサービスにできる。

```
$ cat > tmp-exporter.service << EOF
[Unit]
Description=tmp-exporter
After=network.target

[Service]
Type=simple
ExecStart=python3 /usr/local/bin/tmp-exporter.py
Restart=always

[Install]
WantedBy=multi-user.target
EOF
$ sudo cp tmp-exporter.py /usr/local/bin/
$ sudo cp tmp-exporter.service /etc/systemd/system/
$ sudo systemctl daemon-reload
$ sudo systemctl enable tmp-exporter --now
```

tmp-exporterを起動して、curlで以下の表示がされれば動作している。
データのタイプがcounterでは無くgaugeというのがポイント、かも？日本人的にはcounterを選択しがちだと思うが、値が減る場合があるデータはcounterではいけないとか、何か色々制約がある。
https://prometheus.io/docs/instrumenting/writing_clientlibs/#counter

```
$ curl localhost:8000/metrics
（出力結果）
# HELP tmp_files The number of files in /tmp.
# TYPE tmp_files gauge
tmp_files 3
```

取得される値は/tmpにあるファイルの数なので、以下のコマンドを実行して値を増やしたり減らしたりすることが出来る。

```
$ touch /tmp/aaa
$ rm /tmp/aaa
```

# Prometheusで動作確認
先日の記事（[Prometheusを試してみる](https://qiita.com/rk05231977/items/83232b8b6bacbdeb4629)）を参考にしつつ、tmp-exporterと同じRHEL8サーバーでPrometheusを起動する。
「cat >> prometheus.yml～」の部分がtmp-exporterからのデータを取得を設定する部分となる。

```
$ wget https://github.com/prometheus/prometheus/releases/download/v2.35.0/prometheus-2.35.0.linux-amd64.tar.gz
$ tar xvzf prometheus-2.35.0.linux-amd64.tar.gz
$ cd prometheus-2.35.0.linux-amd64/
$ cat >> prometheus.yml << EOF
  - job_name: "tmp"
    static_configs:
      - targets: ["localhost:8000"]
EOF
$ ./prometheus --config.file=prometheus.yml &
```

RHEL8のVMに9090/tcpポートへのアクセスを許可して（Azureなら仮想マシンのネットワーク設定から）、手元のPCのWebブラウザでVMの9090ポートにアクセスするとPrometheusにアクセスできる。
Graphの画面でExpressionに「tmp_files」と入力して値が取れるorグラフ表示がされれば、自作のExporterは機能している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/907a1e07-602f-73b1-5d6f-9faf574c0712.png)