👀

Prometheus(prom-client) で自作 Exporter を作る

2023/12/19に公開

リポジトリはこちら: ARGI-BERRI/terra

緒言

最近 Switchbot を導入した。具体的なハードウェアは Hub 2Plug Mini である。
Hub 2 は温湿度計・光度計・スマートリモコン機能が統合された機械である。見た目はまんま温湿度計である。Plug Mini はコンセントと電源ケーブルの仲介をすることで物理的に中間者攻撃を実現する(消費電力を測る)という機械である。

Switchbot のハードウェアは社が提供する API を利用することで現在の温湿度や消費電力を取得できる。これは外部から環境の変化を監視できることを意味している。今回は監視システムとして Prometheus を始めとした一連の監視システムを構築する。

システム構成

これらは Docker Compose 環境上に構築する。

  • 自作エクスポーター
    Switchbot のメトリクスの出力に用いる。技術スタックは次のとおりである。
    • TypeScript (実装)
    • esbuild (最終的に Node.js で動かすので、JSへのトランスパイルに用いる)
    • fastify (エクスポーターのアプリサーバ用)
    • siimon/prom-client (Prometheus エクスポーターのお手軽実装ライブラリ)
    • Docker (エクスポーターは最終的にコンテナにする)
  • Prometheus
    データの収集に用いる。システム自体と Prometheus 自身、自作エクスポーターを情報源として用いる。これらのデータは後述の Victoria Metrics に転送される。
  • Victoria Metrics (VM)
    データの集積に用いる。Prometheus はデータの永続化を考えないため VM で集積する。
  • Grafana
    データの表示に用いる。データソースとして Prometheus を利用する。

システム構築:Prometheus 等

docker-compose.yml

Docker エコシステムを用いればすぐに済む。自作エクスポーターだけは、この段階ではまだ動かないので注意する(後の節で実装する)。

https://github.com/ARGI-BERRI/terra/blob/main/docker-compose.yml

prometheus.yml

prometheus.yml も適当に書く。

  • 一旦 Prometheus 自体のメトリクスとシステム全体のメトリクスだけを情報源に設定する。
  • Docker Compose 環境ではコンテナ名を書けばそこに直接アクセスできる。

https://github.com/ARGI-BERRI/terra/blob/main/docker/prometheus/prometheus.yml

システム構築:自作エクスポーター

自作エクスポーターのシステム構成

エクスポーターは最終的に Docker コンテナ化される。

  • fastify
    HTTP サーバー。自作エクスポーターの前段(すなわちリクエストの着信用)に用いる。
  • prom-client
    エクスポーターの Node.js 向け SDK(非公式)。Prometheus メトリクスの算出に用いる。
    TypeScript + esbuild で Node.js 向けにビルドするように仕向ける。

大まかな流れ

(エクスポーターの実装)

  • server.ts (fastify) で適当なアプリサーバを実装する
  • api.ts に Switchbot API からデータを取得する関数を定義する
  • exporter.ts にメトリクスを取得する関数 getMetrics() を定義する
  • server.ts から getMetrics() で現在メトリクスを取得する
  • server.ts がメトリクスをレスポンスとして外部に返却する

(ビルド環境の実装)

  • esbuild でビルドスクリプトを書く
  • Dockerfile を書いてエクスポーター全体をコンテナ化する

fastify の実装

  • /metrics を Prometheus エクスポーターのエンドポイントとする
  • /metricsexporter.ts で定義する getMetrics() をそのまま返す(今は getMetrics() は一旦仮定義でよい)

https://github.com/ARGI-BERRI/terra/blob/d49479ad44d447ff62d52db0bcf77c3459b5e9cc/src/server.ts

自作エクスポーターの実装

アクセス頻度に関して

Switchbot API には一日あたりのアクセス制限がある[1] 。ドキュメントによれば制限は一日あたり一万回である。よって頻繁なアクセスは避けなければならない。

  1. 一日は 86,400 \,[\mathrm{s}] だから、API へのアクセス間隔の上限(これ以上短くしてはならない)は 8.64 \,[\mathrm{s}] である。

API の都合で、情報の取得にあたってはデバイスに結びつく ID 毎にアクセスが必要である。よって、Switchbot デバイスが一台存在するごとに一回の API アクセスが生まれる。

  1. n 台のデバイスが存在する時、アクセス間隔の上限は n \times 8.64 \,[\mathrm{s}] である。

筆者の環境の場合は Hub2 と Plug がそれぞれ一台配備されているため、アクセス頻度の最短時間は 17.28 \,[\mathrm{s}] である。よって、今回の場合は余裕を大きく取ったとして 30 \,[\mathrm{s}] 程度のアクセス間隔に留めなければならない。

API へのアクセス方法に関して

ドキュメントを読んで頂いたほうが早いと思う。

Switchbot API へのアクセスは以下の手続きを踏まなければならない。

  1. Switchbot アプリでトークンと秘密鍵を取得する
  2. トークンと秘密鍵(とノンス・リクエストID)を用いて署名を作成する
  3. API へアクセスするときにはヘッダに署名と諸々のパラメタを与える

詳細は割愛するが、TypeScript での実装は次のとおりである。

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/signature.ts

Switchbot API ラッパの実装

エクスポーターで各種の値を出力するために、Switchbot API からデータを持ってくるラッパを認めなければならない。ラッパの実装は単純であり、(1) ヘッダを前述の方法で生成する (2) Fetch API でデータを API から取得する、という流れである。

まずは API からデータを取得する関数を定義する。API の具体的なエンドポイントは外部から注入する。
https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/api.ts#L6-L14

次に、API から特定のデバイスの状態を取得する関数を定義する。特定のデバイスはデバイス ID として表現される。デバイス ID の取得は次の節で説明する。

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/api.ts#L46-L51

デバイス ID が不詳である場合、API からデバイス ID を取得できる。各デバイスに結びつくデバイス ID は不変であるようなので、API アクセス回数の削減という観点から、デバイス ID は都度取得するのではなく実行環境の環境変数などに定義するといった手が有効である(実際、今回はそうしている)。

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/api.ts#L39-L44

エクスポーター本体の実装

以下の手順で実装を進める。

  1. メトリクスとして表現したい値を prom-client で定義する
  2. getMetrics() にメトリクスを取得する実装を書く

まずはメトリクスとして表現する値を prom-client のクラスで設計する。メトリクスのラベル名が必要であれば labelNames で定義できる。今回は「デバイスの置き場所」をラベル名として表現したかったので、location というラベル名も付せて定義している。

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/exporter.ts#L7-L51

次にメトリクスの取得と fastify に返すためのデータの構築を実装する。実装の見通しを良くするために、getDeviceStatus による API レスポンスは api.ts に定義したインタフェースに強制的にキャストさせている。

  • deviceIds は環境変数(.env)に定義したカンマ区切りのデバイスIDリストである
  • デバイスIDリストの定義は筆者の脳内実装でしかないので、書く順序などに注意する。

(ヘッダ部)
https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/exporter.ts#L1-L2

(メトリクスの計算)
https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/exporter.ts#L53-L70

(インタフェース定義)
https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/src/api.ts#L16-L37

実行環境の実装

  • esbuild スクリプトを書いて、TypeScript → JavaScript にトランスパイルする
  • tsx (TypeScript を実行できる Node.js めいたもの) でスクリプトを実行する
  • スクリプトがビルド結果を吐き出す

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/script/build.ts

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/package.json#L8

エクスポーター全体を Docker 化する

ビルドスクリプトを Dockerfile で実行するだけである。

https://github.com/ARGI-BERRI/terra/blob/a17f1ce6230bc75f2117fd07f4b7b5f67a41cf47/Dockerfile

実行テスト

以下のコマンドで docker-compose.yml を読み込み、コンテナ群を立ち上げる。立ち上がった後、http://localhost:9092 にアクセスして Grafana が表示されたり、Grafana で Switchbot API のメトリクスを読み込めたりしたら成功である。

docker compose up

余談:簡単なデバイスのレビューとか

我が家には古代より伝わる小学校の卒業記念(?)で貰った温湿度計がある。中々長持ちしたものの、運用後半にもなると湿度計の値が常に Lo [2] になってしまった。ほかにもバックライトがないため温度・湿度が見辛いときがあった。

一方 Hub 2 は信頼の置けるスイスだかのセンサとバックライトを搭載している(らしい)。多分精度のよい湿度を出してくれる他、バックライトのお陰で今何度なのか大変見やすい。

ただ、お古の温湿度計にもだいぶ愛着がある。なんだかんだで長持ちしている。
壊れるまでは使っていきたいなと思っている。

脚注
  1. OpenWonderLabs/SwitchBotAPI ↩︎

  2. 湿度 hh < 20 \% であることの表示だと思う。 ↩︎

Discussion