CLOVER🍀

That was when it all began.

PrometheusのNode.jsクライアントを試す

これは、なにをしたくて書いたもの?

PrometheusのNode.js向けのクライアントライブラリ(サードパーティ製のものですが)があるようなので、こちらを試して
みようかと。

Prometheus client for node.js

こちらのライブラリです。

GitHub - siimon/prom-client: Prometheus client for node.js

サードパーティ製ですが、Prometheusのドキュメントにも記載があります。

Client libraries | Prometheus

こちらを使うことで、Node.jsアプリケーションでも、Prometheusで扱える4種類のメトリクス(Counter、Gause、Histogram、Summary)を
独自に出力したり、またこのライブラリがデフォルトでNode.jsランタイムやOSから取得するメトリクスをエクスポートできるように
なるようです。

Prometheusのデータモデルや、メトリクスの種類についてはこちら。

Data model | Prometheus

Metric types | Prometheus

このブログでは、以前にJVM Clientを使った時にこのあたりの用語については書いておきました。

PrometheusのJVM Clientを試してみる - CLOVER🍀

それで、このNode.jsクライアントですが、今回はExpressと一緒に使おうと思います。とはいえ、このライブラリが
特にWebアプリケーション用、というわけではありません。Registryのメソッドを使うことで、いつでもメトリクスを
出力することができます。

それでは、使っていってみましょう。お題は、以前に書いたJVM Clientのサンプルの簡易版にします。

環境

今回の環境は、こちらです。

$ node -v
v10.15.3

$ npm -v
6.4.1

Prometheus client for node.jsをインストール。

$ npm i prom-client

Expressもインストールします。

$ npm i express

インストールされたバージョンはこちら。

  "dependencies": {
    "express": "^4.16.4",
    "prom-client": "^11.2.1"
  },

サンプルプログラム

作成したプログラムは、こちら。
server.js

const prometheusClient = require("prom-client");
const collectDefaultMetrics = prometheusClient.collectDefaultMetrics;

collectDefaultMetrics();  // デフォルトで組み込まれているメトリクスを、デフォルト10秒間隔で取得
// collectDefaultMetrics({ timeout: 5000 });  // デフォルトで組み込まれているメトリクスを、5秒おきに取得

const express = require("express");
const app = express();

let counter = 0;
const callCounter = new prometheusClient.Counter({
                        name: "method_call_counter",
                        help: "counter for method call count",
                        labelNames: ["method", "path"]
                    });
const incRequestSummary = new prometheusClient.Summary({
                        name: "inc_request_summary",
                        help: "summary for inc method call summary",
                    });

app.get("/metrics", (req, res) => {
    res.set("Content-Type", "text/plain");
    res.send(prometheusClient.register.metrics());
});

app.get("/counter/current", (req, res) => {
    callCounter.labels("inc", "/counter/current").inc();
    res.send(`current counter = ${counter}`);
});

app.get("/counter/inc", (req, res) => {
    const end = incRequestSummary.startTimer();
    try {
        counter++;
        callCounter.labels("inc", "/counter/inc").inc();
        res.send(`increment counter = ${counter}`);
    } finally {
        end();
    }
});

app.listen(3000, () => console.log(`[${new Date()}] server startup.`));

ドキュメントに沿って、見ていきます。

API

利用するモジュールをrequireしておきます。

const prometheusClient = require("prom-client");

また、デフォルトで利用できるメトリクスを有効にします。

const collectDefaultMetrics = prometheusClient.collectDefaultMetrics;

collectDefaultMetrics();  // デフォルトで組み込まれているメトリクスを、デフォルト10秒間隔で取得
// collectDefaultMetrics({ timeout: 5000 });  // デフォルトで組み込まれているメトリクスを、5秒おきに取得

デフォルトで収集されるメトリクスは、有効にしておくことが推奨されています。

Default metrics

ここでいう、「デフォルトのメトリクス」とは、このあたりですね。

const metrics = {
    processCpuTotal,
    processStartTime,
    osMemoryHeap,
    processOpenFileDescriptors,
    processMaxFileDescriptors,
    eventLoopLag,
    processHandles,
    processRequests,
    heapSizeAndUsed,
    heapSpacesSizeAndUsed,
    version
};

イベントループやプロセスハンドル、Node.jsのバージョンなど、Node.js固有のメトリクスも含まれています。

https://github.com/siimon/prom-client/blob/v11.2.1/lib/defaultMetrics.js#L3-L13

https://github.com/siimon/prom-client/blob/v11.2.1/lib/defaultMetrics.js#L17-L29

デフォルトのメトリクスは実行する際にオプションを指定することができ、今回のコメントではメトリクスの取得間隔を
しているサンプルにしていますが、他にはメトリクスのprefixやメトリクスの登録対象のRegistryを指定したりできるようです。

collectDefaultMetrics();  // デフォルトで組み込まれているメトリクスを、デフォルト10秒間隔で取得
// collectDefaultMetrics({ timeout: 5000 });  // デフォルトで組み込まれているメトリクスを、5秒おきに取得

で、今回はExpressを使いつつ、カウンターを用意してカウントアップするAPIと現在のカウンターの値を取得するAPI
用意してみましょう。

app.get("/counter/current", (req, res) => {
    callCounter.labels("inc", "/counter/current").inc();
    res.send(`current counter = ${counter}`);
});

app.get("/counter/inc", (req, res) => {
    const end = incRequestSummary.startTimer();
    try {
        counter++;
        callCounter.labels("inc", "/counter/inc").inc();
        res.send(`increment counter = ${counter}`);
    } finally {
        end();
    }
});

この時、メトリクスも登録していくようにします。今回は、CounterとSummaryの2つを使用しました。

let counter = 0;
const callCounter = new prometheusClient.Counter({
                        name: "method_call_counter",
                        help: "counter for method call count",
                        labelNames: ["method", "path"]
                    });
const incRequestSummary = new prometheusClient.Summary({
                        name: "inc_request_summary",
                        help: "summary for inc method call summary",
                    });

Counter

Summary

Counterは用意した2つのメソッド呼び出しの回数を、ラベルを使いつつ記録します。Summaryはカウントアップのみを対象に、
ラベルはなしで使います。

ラベルを使う場合は、このようにlabelsメソッドを介して目的のメソッドを呼び出します(Counterならincなど)。

app.get("/counter/current", (req, res) => {
    callCounter.labels("inc", "/counter/current").inc();
    res.send(`current counter = ${counter}`);
});

また、ドキュメントを見ると値の設定とともにラベルを指定することもできるようですね。

Labels

Summaryはstart/end方式のようです。ラベルは使っていません。
※一緒にCounterも使っています。

app.get("/counter/inc", (req, res) => {
    const end = incRequestSummary.startTimer();
    try {
        counter++;
        callCounter.labels("inc", "/counter/inc").inc();
        res.send(`increment counter = ${counter}`);
    } finally {
        end();
    }
});

他にもSummary#observeなどあるので、ドキュメントを参照してみてください。

Summary

登録しているメトリクスの出力には、Registry#metricsメソッドを使います。

今回は、Expressを使ってエンドポイントを設けました。

app.get("/metrics", (req, res) => {
    res.set("Content-Type", "text/plain");
    res.send(prometheusClient.register.metrics());
});

あとは、このクラスを「npm start」で起動できるようにしましょう。

  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

これで、サンプルプログラムの準備は完了です。

確認

とりあえず、起動してみます。

$ npm start

まずは、メトリクスを取得してみます。

$ curl localhost:3000/metrics
$ curl localhost:3000/metrics
# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter
process_cpu_user_seconds_total 0.001038 1552119478037

# HELP process_cpu_system_seconds_total Total system CPU time spent in seconds.
# TYPE process_cpu_system_seconds_total counter
process_cpu_system_seconds_total 0.00026 1552119478037

# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 0.001298 1552119478037

# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1552119478

# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 41758720 1552119478090

〜省略〜

# HELP method_call_counter counter for method call count
# TYPE method_call_counter counter

# HELP inc_request_summary summary for inc method call summary
# TYPE inc_request_summary summary
inc_request_summary{quantile="0.01"} 0
inc_request_summary{quantile="0.05"} 0
inc_request_summary{quantile="0.5"} 0
inc_request_summary{quantile="0.9"} 0
inc_request_summary{quantile="0.95"} 0
inc_request_summary{quantile="0.99"} 0
inc_request_summary{quantile="0.999"} 0
inc_request_summary_sum 0
inc_request_summary_count 0

いろいろくっついていますが、最後に自分が登録したメトリクスもすでに一部見えています。

デフォルトのメトリクスの最後についている数字ですが、これは値を設定した際のタイムスタンプです。

Timestamps

これは、自分で作成したメトリクスにも記録することができます(明示的に指定が必要で、今回は指定していません)。

Register#metricsを呼び出す際に、タイムスタンプを出力しないようにすることもできるようです。

Register

それでは、今回作成したAPIを呼び出してみましょう。

$ curl localhost:3000/counter/inc
increment counter = 1
$ curl localhost:3000/counter/inc
increment counter = 2
$ curl localhost:3000/counter/inc
increment counter = 3
$ curl localhost:3000/counter/current
current counter = 3
$ curl localhost:3000/counter/inc
increment counter = 4
$ curl localhost:3000/counter/inc
increment counter = 5
$ curl localhost:3000/counter/inc
increment counter = 6
$ curl localhost:3000/counter/current
current counter = 6

「/counter/inc」を6回、「/counter/current」を2回呼び出してみました。

メトリクスを取得してみます。

$ curl localhost:3000/metrics
〜省略〜

# HELP method_call_counter counter for method call count
# TYPE method_call_counter counter
method_call_counter{method="inc",path="/counter/inc"} 6
method_call_counter{method="inc",path="/counter/current"} 2

# HELP inc_request_summary summary for inc method call summary
# TYPE inc_request_summary summary
inc_request_summary{quantile="0.01"} 0.000247481
inc_request_summary{quantile="0.05"} 0.000247481
inc_request_summary{quantile="0.5"} 0.000328285
inc_request_summary{quantile="0.9"} 0.002689974700000001
inc_request_summary{quantile="0.95"} 0.002912271
inc_request_summary{quantile="0.99"} 0.002912271
inc_request_summary{quantile="0.999"} 0.002912271
inc_request_summary_sum 0.004805055
inc_request_summary_count 6

今回作成したCounterでは、各メソッドの呼び出し回数がラベルとともに表示され

method_call_counter{method="inc",path="/counter/inc"} 6
method_call_counter{method="inc",path="/counter/current"} 2

Summaryでは呼び出し回数と分位数(quantile)が表示されています。

inc_request_summary{quantile="0.01"} 0.000247481
inc_request_summary{quantile="0.05"} 0.000247481
inc_request_summary{quantile="0.5"} 0.000328285
inc_request_summary{quantile="0.9"} 0.002689974700000001
inc_request_summary{quantile="0.95"} 0.002912271
inc_request_summary{quantile="0.99"} 0.002912271
inc_request_summary{quantile="0.999"} 0.002912271
inc_request_summary_sum 0.004805055
inc_request_summary_count 6

まとめ

今回は、PrometheusのNode.js向けのクライアント(Prometheus client for node.js)を試してみました。

メトリクスの出力方法が最初わかりにくいかな?と思ったくらいで、扱っている概念はPrometheusのものなのでそれほど苦労せず
使えました。

これで、Node.jsアプリケーションでもPrometheus向けにメトリクスがエクスポートできるようになりましたね。