フロントエンド・ダッシュボードの構築(第二回):Vue、Node、GridDBでサーバーモニターを作る

多くのダッシュボードアプリは、ユーザーに対して時系列データを可視化する必要があります。このデータは、価格情報であったり、ウェブ解析であったり、その他想像できるものです。

このチュートリアルでは、Node.js サーバー用のサーバーメモリーモニターを作成します。これは、1秒ごとにシステム・メモリの読み取りを行い、データベースに保存します。

このデータは、フロントエンドの Vue アプリでダッシュボードの視覚化とともに表示されます。

また、時系列データの書き込みと取得が簡単にできるGridDBを使用します。データは、Expressフレームワークを使って、パブリックAPIとして公開します。

Dockerがインストールされており、Vue、Node.js、GridDBの基本的な知識を持っていることを前提に説明します。

完全なコードはこのリポジトリでアクセスできます。

Dockerを使った環境構築

前回のチュートリアルでは、GridDBに接続するNode & Express APIのセットアップを説明しました。Dockerコンテナを利用することで、どのOS上でも簡単にGridDBを起動することができます。

このチュートリアルでは、Dockerコンテナを利用することで、簡単にGridDBを起動することができます。このチュートリアルのセットアップをさらに理解する必要がある場合は、前回のチュートリアルを参照することをお勧めします。

前回のチュートリアルに従わない場合は、あなたのコンピューターに このレポジトリ をクローンしてください。


$ git clone https://github.com/anthonygore/node-griddb-docker.git

ここで、新しく作成したディレクトリに入り、Docker Composeを実行して、仮想環境をインストールし、起動します。


$ cd node-griddb-docker
$ docker-compose up --build

インストールが完了すると、Express サーバが GridDB データベースインスタンスと同時に動作するようになります。

Curl で Express サーバにリクエストを行い、正常に動作していることを確認します。


$ curl http://localhost:3000

コンソールに “Hello, World “のメッセージが表示されるはずです。

システムメモリの読み取りを行う

今回の目的は、サーバーの空きメモリを計測し、そのデータをAPIとして提供するアプリを作成することです。

Node パッケージの node-os-utils を使えば、サーバー上の空きメモリの量を簡単に取得できます。

まずはこれをインストールしましょう。


$ npm i -S node-os-utils

新しいNPMの依存関係を追加したので、Dockerコンテナを再起動する必要があります。Ctrl+Cで現在のコンテナを kill してから、docker build --rm .を実行して、新しい依存関係のあるイメージを再ビルドし、以前のイメージを削除します。その後、docker-compose upを実行して、コンテナを再起動します。

Node OS Utilsを使用して、Express APIがパス /data にある空きメモリの量を返すようにしましょう。

server.js


const osu = require('node-os-utils')

...

app.get('/data', async (req, res) => {
  const info = await osu.mem.info()
  res.send(info);
});

再びターミナルでCurlを使ってテストしてみましょう。今度は、システム・メモリに関する統計がレスポンスに表示されます。


$ curl http://localhost:3000/data

# {"totalMemMb":1986.16,"usedMemMb":848.47,"freeMemMb":1137.69,"usedMemPercentage":42.72,"freeMemPercentage":57.28}

インターバル・メモリー・リーディング

モニターを作るのであれば、時系列の値をデータベースに保存して、特定の間隔で表示できるようにしたいと思います。

そのために、サーバーが起動してから1秒ごとに値を取得することにします。これには setInterval を使いましょう。今のところ、これらの値をコンソールに表示するだけです。

server.js


setInterval(async() => {
  const info = await osu.mem.info();
  console.log(info.freeMemPercentage);
}, 1000);

コンテナを作成する

さて、このデータをどうやってデータベースに取り込むのでしょうか?まず必要なのは、コンテナを作成することです。

そのために、”FreeMemoryPercentage”というGridDBの時系列コンテナのスキーマを定義します。これは2つのカラムを持つコンテナになります。timestampは当然TIMESTAMP型で、freeMemPercentageDOUBLE型になります。

そして、ストアの putContainer メソッドを呼び出して、コンテナを作成します。


const containerName ="FreeMemoryPercentage";
const schema = new griddb.ContainerInfo({
    name: containerName,
    columnInfoList: [
    ["timestamp", griddb.Type.TIMESTAMP],
    ["freeMemPercentage", griddb.Type.DOUBLE]
  ],
    type: griddb.ContainerType.TIME_SERIES,
    rowKey: true
});
const container = await store.putContainer(schema, false);

では、このコードをプロジェクトのどこに置けばいいのでしょうか?データベースファイルに移動して、関数 createContainer を作成しましょう。ここでは、まず store.getContainer を呼び出してコンテナを取得しようとします(すでに作成されている可能性もありますが)。

もしコンテナが null ならば、上記のコードでコンテナを作成します。

これが完了したら、 connect メソッドからこのメソッドを呼び出して返します。これにより、コンテナにストアを渡すことができ、データベースへのアクセスのためのAPIを作成することができます。

db.js


const createContainer = async (store) => {
  let container = await store.getContainer(containerName);
  if (container === null) {
    try {
      const schema = new griddb.ContainerInfo({
                name: containerName,
                columnInfoList: [
            ["timestamp", griddb.Type.TIMESTAMP],
            ["freeMemPercentage", griddb.Type.DOUBLE]
          ],
                type: griddb.ContainerType.TIME_SERIES,
                rowKey: true
      });
      container = await store.putContainer(schema, false);
    } catch (err) {
            console.log(err);
    }
  }
}

const connect = async () => {
  ...
  createContainer(store);
};

メモリの読み取り値をコンテナに格納する

これで、データを入れるコンテナができました。このコンテナには、どのように空きメモリの値を格納するのでしょうか。

そのために、データベースファイルに putRow という別の関数を作成します。これを高階関数にすることで、consumerが気にすることなく自動的にコンテナにデータを渡せるようにします。

返された関数では、まずタイムスタンプを作成します。秒が基本的な間隔であるため、直近の秒に丸めることに注意してください。そして、そのタイムスタンプと、関数に与えられたあらゆる値を、container.putを使ってコンテナに格納します。

db.js


const putRow = (container) => async (val) => {
  try {
    const p = 1000;
    const now = new Date();
    const time = new Date(Math.round(now.getTime() / p ) * p);
    await container.put([time, val]);
  } catch (err) {
    console.log(err);
  }
}

今度は createContainer メソッドから putRow の内部関数を返して、メインサーバーファイルで使用できるシンプルな API を作成します。

db.js


const createContainer = async (store) => {
  ...
  return {
    putRow: putRow(container)
  }
}

Express サーバーから読み取ったメモリーを保存する

それでは、サーバーファイル内のサーバーコンテナにアクセスできるようにしましょう。そのために、変数 container を宣言しますが、まだ初期化しません。

次に、データベースの API を返す db.connect メソッドを呼び出します。これは非同期関数なので、プロミスが解決されるまで待ち、その値を先ほど作成した変数に代入する必要があります。

server.js


let container;
db.connect().then(c => container = c);

さて、インターバルコールバックでは、APIメソッド container.putRow を使って、毎秒のメモリ読み取り値を格納することができます。


setInterval(async() => {
  const info = await osu.mem.info();
  await container.putRow(info.freeMemPercentage);
}, 1000);

最新の測定値を取得する

このアプリを作成する次のステップは、最新の測定値を返すことです。これを行うには、データベースファイルに戻り、別の関数 getLatestRows を作成します。ここでも高階の関数とし、コンテナのインスタンスをきちんと渡せるようにします。

この関数では、GridDB コンテナに対するクエリを TQL 構文で作成します。このクエリは、タイムスタンプが過去5分以内のエントリーを取得します。

そして、返された行をそれぞれ処理し、一つの配列としてデータを返します。

この新しいメソッドは createContainer メソッドからも呼び出され、別の API を提供します。

db.js


const getLatestRows = (container) => async () => {
  try {
    const query = container.query(
            "select * where timestamp > TIMESTAMPADD(MINUTES, NOW(), -5)"
        );
    const rowset = await query.fetch();
    const data = [];
    while (rowset.hasNext()) {
      data.push(rowset.next());
    }
    return data;
  } catch(err) {
    console.log(err);
  }
}

const createContainer = async (store) => {
  ...
  return {
    putRow: putRow(container),
    getLatestRows: getLatestRows(container)
  }
}

サーバーファイルに戻り、Express ルートハンドラ内でこの新しいメソッドを呼び出して、データを返すことができます。

server.js


app.get('/data', async (req, res) => {
  const rows = await container.getLatestRows();
  res.send(rows);
});

これをテストするために、再びCurlを使用することができます。フロントエンドアプリで消費する準備ができたデータブロックが表示されるはずです。


$ curl http://localhost:3000/data

# [["2021-10-22T07:05:52.000Z",57.53],...]

index.html ファイルを提供する

それでは、空きメモリのデータを可視化するためのフロントエンドアプリを作成してみましょう。

まず、Express サーバーが Hello, World メッセージではなく、ルートパスにある HTML ドキュメントを返すようにします。

server.js


const path = require('path');

...

app.get('/', async (req, res) => {
  res.sendFile(path.join(__dirname, '/index.html'));
});

次に、index.html ファイルを作成する必要があります。この基本的なHTML定型文を使用することができます。

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Server Monitor</title>
  <meta name="description" content="Node and GridDB server monitor">
</head>
<body>
</body>
</html>

npm startスクリプトを変更し、NodemonがこのHTMLファイルを監視するようにすることをお勧めします。

これで、開発中にindex.htmlに変更を加えると、サーバーは自動的に再起動します。

package.json


"scripts": {
  "start": "nodemon server.js -e js,html"
},

この後、Dockerコンテナを一旦終了させ、docker-compose up --build で再起動します。

さて、ブラウザを開いて、http://localhost:3000 にアクセスしてみてください。HTMLドキュメントが読み込まれているのが見えると思いますが、まだコンテンツは表示されていません。

フロントエンドアプリの作成

これから、HTML文書の先頭に4つのスクリプトを追加していきます。Moment.js、Vue.js、Chart.js、VueChart.jsです。これらはすべて、時系列のビジュアライゼーションを表示するために必要なものです。

index.html

<head>
    ...
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
    <script src="https://unpkg.com/vue-chartjs/dist/vue-chartjs.min.js"></script>
  </head>

ドキュメント本体には、Vueアプリのmount要素をID appで作成します。そして、Vueアプリを宣言するscriptタグを作成しましょう。

index.html

<div id="app"></div>
<script type="text/javascript">
  new Vue({
    el: "#app"
  });
</script>

このアプリで最初に行うことは、fetch を使って mounted ライフサイクルフックにあるサーバーモニターデータを取得することです。これをデータプロパティ serverData に代入し、xy の値のオブジェクトにマッピングします。

index.html


new Vue({
  el: "#app",
    data: () => ({
    serverData: []
  }),
  async mounted () {
    const res = await fetch("/data");
    const data = await res.json();
    this.serverData = data.map(row => ({ x: row[0], y: row[1] }))
  }
});

チャートコンポーネント

ここで、新しい Vue コンポーネント server-monitor を宣言します。これは、VueChartJsの Line コンポーネントを継承します。このコンポーネントのpropに serverData を指定し、まもなくアプリから渡します。

ライフサイクルフックの mounted では、VueChart.js が時系列データを見やすく表示するために必要な設定を作成します。

VueChart.jsの設定についての詳細は、こちらを参照してください。

index.html


Vue.component('server-monitor', {
    extends: VueChartJs.Line,
    props: ['serverData'],
    mounted () {
      const chartData = {
        labels: [],
          datasets: [
          {
            label: 'Free Memory %',
            backgroundColor: '#f87979',
            data: this.serverData,
            fill: false,
            borderColor: 'rgb(75, 192, 192)',
            tension: 0.1
          }
        ]
      };
      const options = {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          xAxes: [{
            type: "time",
            distribution: "linear"
          }],
          title: {
            display: false
          }
        }
      }
      this.renderChart(chartData, options)
    }
  })

最後に、サーバーのデータが入力されていることを条件として、アプリのコンテンツにチャートコンポーネントを宣言します。

index.html


<div id="app">
    <server-monitor v-if="serverData.length" :server-data="serverData" />
  </div>

これで、サーバーモニターがページ上にレンダリングされるのが確認できるはずです。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *