Node.jsをOpenTelemetryでメトリック収集してみる

# はじめに

前回、Node.js(JavaScript)でOpenTelemetryによるトレースデータ収集を行なったので、今度はカウンタ値のようなメトリックデータ収集についてまとめてみました。

OpenTelemetryの概要やトレースデータ収集の記事は[こちら](https://qiita.com/raichi/items/3cd06d8c216eb82d54c4)を参照。

# やってみること

Node.jsアプリケーション上で収集したメトリックデータ（今回は３種類のカウンタの数値）を、Prometheusでグラフ化してみます。
![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94561/22fcef5f-85c4-94a0-ab23-c4b378e6d6c1.png)

# 実行環境

* MacOS X
* node: v13.8.0
* npm: v7.5.3
* express: v4.17.1
* OpenTelemetry: v0.18

# 1. サンプル構成の作成

まずはベースとなるサンプルアプリケーションを作成します。<br>
サンプルの内容は、URLにアクセスすると３種類の色（red, blue, yellow）のいずれかを返す単純なロジックとなります。

## 1.1 サンプル作成

プロジェクトにExpressをインストールします。

```shell
$ npm install express
```

アプリケーション(app.js)を作成します。

```javascript:app.js
'use strict';

const express = require('express');
const app = express();
const PORT = process.env.PORT || '8080';

app.use(function(req, res, next) {
  // ランダムで色を選ぶ
  const color = ['red', 'blue', 'blue', 'yellow', 'yellow', 'yellow'];
  res.locals.color = color[Math.floor(Math.random() * color.length)];
  next();
});

app.get('/', (req, res, next) => {
  res.send(res.locals.color);
  next();
});

app.listen(parseInt(PORT, 10), () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});
```

## 1.2 サンプルの動作確認

サンプルを実行すると、localhost:8080でクライアントからのリクエストを待ち受けるようになります。

```shell
$ node app.js
Listening for requests on http://localhost:8080
```

別のターミナルからサンプルのURLにアクセスします。

```shell
$ curl localhost:8080
red
```

上記のようにred, blue, yellowのいずれかが返却されます。


# 2. Prometheusの起動

メトリック収集の準備として、メトリックデータのグラフ化を行うPrometheusを構築します。<br>
今回はDockerコンテナ版を使用します。

コンテナ起動前に、まずprometheus.ymlを作成します。

```yaml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'opentelemetry'
    static_configs:
      - targets: 
        - 'host.docker.internal:9464'
```

targetsはPrometheusにおけるメトリックデータの収集先となります。
今回はホストとなりますが、コンテナから見たホストはlocalhostではなくhost.docker.internalとなります（localhostはコンテナを指す）。
なおhost.docker.internalはLinuxでは使えないため（MacやWindowsなら可）、Linux環境の場合はホストのIPアドレスに置き換えてください。


```shell
docker run \
    -d \
    -p 9090:9090 \
    -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus
```

localhost:9090にアクセスし、PrometheusのGUI画面が表示されたら起動OKです。

![prometheus1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94561/9f52b686-eac3-3c56-8c96-411561b61bb0.png)

# 3. OpenTelemetryの組み込み

それでは先ほど作成したサンプルに、メトリックデータ収集を行うOpenTelemetryを組み込んでいきます。

## 3.1 パッケージのインストール

```shell
$ npm install @opentelemetry/metrics @opentelemetry/exporter-prometheus
```

## 3.2 monitoring.jsの作成

サンプルに組み込むカウンタのロジックを作成します。

```javascript:monitoring.js
'use strict';

const { MeterProvider } = require('@opentelemetry/metrics');
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');

const prometheusPort = PrometheusExporter.DEFAULT_OPTIONS.port
const prometheusEndpoint = PrometheusExporter.DEFAULT_OPTIONS.endpoint
const exporter = new PrometheusExporter(
  {
    startServer: true,
  },
  () => {
    console.log(
      `prometheus scrape endpoint: http://localhost:${prometheusPort}${prometheusEndpoint}`,
    );
  },
);

const meter = new MeterProvider({
  exporter,
  interval: 1000,
}).getMeter('color-meter');

const colorCount = meter.createCounter('colors', {
  description: 'Count each color'
});

// 色ごとにCounterを生成する
const redCounter = colorCount.bind({ color: 'red'});
const blueCounter = colorCount.bind({ color: 'blue'});
const yellowCounter = colorCount.bind({ color: 'yellow'});

module.exports.countColorRequests = () => {
  return (req, res, next) => {
    switch (res.locals.color) {
      case 'red':
        redCounter.add(1);
        break;
      case 'blue':
        blueCounter.add(1);
        break;
      case 'yellow':
        yellowCounter.add(1);
        break;
    }
    next();
  };
};
```

カウンタはmeter.createCounter()で作成します。<br>
引数で指定した文字列が、メトリックデータのテーブル名に相当します。

また今回は返却した色ごとのカウンタ値をグラフ化したいので、以下のようにmeter.createCounter()で作成したカウンタにラベルをbindし、３色分のカウンタを作成します。
ここで指定する color: 'red'はグラフの系列名に相当します。

```
const redCounter = colorCount.bind({ color: 'red'});
```

## 3.3 サンプルへの組み込み

先ほど作成したmonitoring.jsをサンプルに組み込みます。

```diff_javascript:app.js
'use strict';

const express = require('express');
const app = express();
const PORT = process.env.PORT || '8080';

+ const { countColorRequests } = require("./monitoring");

app.use(function(req, res, next) {
  // ランダムで色を選ぶ
  const color = ['red', 'blue', 'blue', 'yellow', 'yellow', 'yellow'];
  res.locals.color = color[Math.floor(Math.random() * color.length)];
  next();
});

+ app.use(countColorRequests());

app.get('/', (req, res, next) => {
  res.send(res.locals.color);
  next();
});

app.listen(parseInt(PORT, 10), () => {
  console.log(`Listening for requests on http://localhost:${PORT}`);
});
```

Math.random()で色を選択した後、countColorRequests()を呼び出し、選択した色のカウンタをインクリメントしています。

## 3.4 動作確認

1章と同様に、サンプルを起動します。
起動すると、localhost:8080のリクエスト待ち受けに加え、localhost:9464/metricsでもPrometheusからのメトリックデータ収集(Pull)を待ち受けるようになります。

```shell
$ node app.js 
Listening for requests on http://localhost:8080
prometheus scrape endpoint: http://localhost:9464/metrics
```

サンプルへのリクエスト送信を100回連続で行い、カウンタ値を増やしてみます。

```
$ for var in `seq 100`; do curl localhost:8080; done
```

カウンタ値は、localhost:9464/metricsにアクセスすると参照できます。<br>（Prometheusはこの内容を定期的に収集します）

```shell
$ curl http://localhost:9464/metrics
# HELP colors Count each color
# TYPE colors counter
colors{color="red"} 14 1615214799584
colors{color="blue"} 32 1615214799558
colors{color="yellow"} 54 1615214799597
```

それではPrometheusでグラフ表示してみます。

Graphタブを選択し、テキストボックスにcolorsと入力、Executeボタンを実行します。
すると、下図のようなグラフが出力され、カウンタ値のインクリメントの推移を確認することができます。

![prometheus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94561/60a3f097-a5a7-d92b-ae1d-d6be99df5165.png)

# おわりに

メトリック収集はOpenTelemetryを使わなくてもさまざまな方法で実現できますが、ユーザアプリケーション独自のメトリックを収集する場合にOpenTelemetryは強みを発揮しそうです。

OpenTelemetryについて、他にも面白そうな機能があれば試していきたいと思います。