MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

Grafanaの画面操作を Playwright を使って便利にする

京都研究所で監視チームのお手伝いをさせてもらってるエンジニアの I です。マイクロアドでは監視の可視化ツールとしてGrafanaやKibanaを活用しています。今回は普段Grafanaを使っていて不便な点を補うためにコードを書いて解決した話を紹介します。

Grafana の library panels

社内で監視用の可視化で利用しているGrafanaで、最近のバージョン8になってlibrary panelsという機能が導入されました。
grafana.com

このlibrary panelsはパネル単位で作ったグラフを再利用するものですが、パネルはどこかのダッシュボード内に作る時にダッシュボード側の変数を参照して作成されている事が多いのですが、単純に既存のパネルをライブラリ的に組み合わせても、変数の部分が動作しないことも多く、簡単な組み合わせで表示できる範囲が限られるのではないかと思います(別種のパネルを混合できるのは変数の定義が共通化できている範囲内)。

例えば、何かシステムに障害が起こっているときなど、頻繁に発生する作業としては、ダッシュボードAの1番目のパネルとダッシュボードBの2番めのパネルを同じ時刻で表示させてみて、システム側で起こっていることを推測する、という作業があります。また障害の後に(または普段から)、定期的に同じような手順を繰り返すので、パネル単位の再利用というよりも、単に異なるパネルの時刻を揃えて閲覧したくなります。library panelsではこのA,Bの組み合わせから新規ダッシュボードCを作るのが大変になるケースもあるため、任意のパネルの時刻を揃えて表示するにはどうすればいいかを調べたのですが、近いものとして、別サイトに 埋め込む という方法がありましたが、目的のものは見つかりませんでした。また、Scripted dashboards という方法があります。自分でコードを用意して表示する仕組みで非常に自由度が高いのですが将来廃止予定とされていました。

panel の表示を便利にしたい

欲しいのは単純に以下の機能です。Grafanaを見る時には固定したルーティンワークになっている気がします。

  1. ダッシュボードAの1番目のパネルとBの2番目で表示する時刻を揃える
  2. 表示されたグラフを残すためにURLか画像を記録する

これはGrafanaというよりもGrafanaでページを見ている時にブラウザ側に欲しいタイプのものです。ブラウザ側なのでどうすればよいか。単純に考えてブラウザ拡張のAPIを使うことになると思いますが、拡張APIの組み合わせによる開発は本格的過ぎます(拡張の開発に興味があれば楽しいとは思います)。そこで、開発が活発なwebのブラウザテストのフレームワークを使うことを考えてみました(フレームワークは例えばCypress、WebdriveIO、Playwrightなどがあります)。

こちらはブラウザ操作の自動化や画面の画像を残す作業がメインのタスクであることから、上記で書いたような作業に対応する機能が予め用意されています。今回はこのブラウザテスト用のフレームワークを使って、運用業務のルーティンワークの1の部分をまず簡素化してみました。わざわざテストフレームワーク等を使わなくても、同時に見たいグラフを1つのダッシュボードに集めれば済む話ですが、少ないコードでパネルを同時に見る作業が改善したので紹介します。

ブラウザ側で機能するプログラム

テストのフレームワークは例としてPlaywrightを使っています。こちらが対象とするページにアクセスし画面の画像を記録する単純なコードの例です。

また、今回のやりたいことを実装するのに必要な機能は以下2つです。

  1. browserのjavascriptからメインプロセスの関数を実行する仕組み1
  2. 対象ページにアクセスした際、自分が用意したjavascriptをブラウザに実行させその後ページをロードする初期化の仕組み2

この2つの機能を使って図のように2つのタブの間の時刻を同期します。

f:id:vilevan:20220314124206p:plain

まず、main.jsというファイルをnode.jsから起動するjavascriptとして用意します。これがOSにアクセスできるメイン側のプロセスになります。ブラウザ内で実行され、documentにアクセス可能なjavascriptのコードの側から、メイン側(node.js)の処理を呼び出すのですが、画面キャプチャの機能や2つのページ(タブ)間で情報を共有する機能は共にメイン側(node.js)でPlaywrightの機能を使います。一方でブラウザ側のjavascriptで行う処理としては、ボタンをクリックするなどの画面操作やユーザのアクション(ブラウザのキーボードショートカットなど)による関数の呼び出しなどがあります。

そのために、ページを読み込む時にブラウザ側でjavascriptを実行させておく必要がありますが、初期化の仕組みでショートカットを事前に定義してページをロードさせます。

ここでショートカットを定義する以下のjavascriptを shortcut.js として用意します。

async function handleShortCutKey(e) { 
      if ('KeyB'==e.code && e.ctrlKey) { // ctrl + b
          // 後で追記
      }
}
document.addEventListener('keyup', handleShortCutKey); 

上記をブラウザに渡すのが次のコードです。

  await context.addInitScript({ path: 'shortcut.js' });

メインプロセス側(main.js)に記述しておくと、 http://localhost:3000/shortcut.js のようにまずshortcut.jsがロードされた後でgrafanaのページの初期化が行われます。

$ node main.jsのように起動

main.js (node.jsからchromiumを起動)
       -> chromiumがgrafanaサーバへ接続し、ブラウザ側でshortcut.jsが動く
          -> grafana側のページの通常の初期化が行われる

context、pageという用語が出てきますが、pageはそれぞれ個別の内容の表示を担当し、すべてのpageに共通の処理は上位のcontextに定義します。

f:id:vilevan:20220307100535p:plain

これを踏まえ、page間で情報を共有するためにpage1の日付の情報をpage2に渡します(main.js)。

  const page1 = await context.newPage();
  const page2 = await context.newPage();
  page1.exposeBinding('getPageURL', ({ page1 }) => { return page1.url(); });

  page1.exposeBinding('copyDate', async ({ page1 }, from, to) => {
    await page2.locator('[aria-label="TimePicker\\ Open\\ Button"]').click();
    await page2.locator('[aria-label="TimePicker\\ from\\ field"]').fill(from);
    await page2.locator('[aria-label="TimePicker\\ to\\ field"]').fill(to);
    await page2.locator('[aria-label="TimePicker\\ submit\\ button"]').click();
    return 0;
  });

この関数をbrowser側から呼び出して、page1で表示している時刻の値を渡します。page2の方で渡された情報を使って画面操縦(click/fill)していますが、この部分はcodegenというツールで生成させています。

これを呼び出すところを完成させます。page1で表示しているグラフの時刻がURLに保持されているのでURLから時刻を取り出して渡しています(shortcut.js)。

function convDateFormat(str){
      // 省略
}
async function handleShortCutKey(e) { 
      if ('KeyB'==e.code && e.ctrlKey) { // ctrl + b
         const pageurl = await getPageURL();
         const from_to = pageurl.match(/http:\/\/localhost:3000\/.*?\?.*?&from=(?<from>.+?)&to=(?<to>.+?)(&.*)?$/, '$2');
         const from = convDateFormat(from_to.groups["from"]);
         const to = convDateFormat(from_to.groups["to"]);
         const _ = copyDate(from, to);
      }
}
document.addEventListener('keyup', handleShortCutKey); 

ルーティンワークの2の画面キャプチャを行うところはpage.screenshot()を使うだけで他は同じ流れなので割愛します。 以下は2つのタブで時刻を同期させた時の操作の様子です。2つ目のタブの右上の時刻部分が、1つ目のタブの時刻に同期しているのが分かります。

f:id:vilevan:20220314112651g:plain

最後に

Grafanaには image render という機能が内蔵されており、それに関連してグラフの画像を取得するためのURLが用意されています。ver7.4.3の仕様は以下でした。

  1. http://grafana-url:3000/d/abcdefg/node-exporter?viewPanel=77&orgId=1&from=1234&to=5678 (元URL)
  2. http://grafana-url:3000/d-solo/abcdefg/node-exporter?orgId=1&from=1234&to=5678&panelId=77 (埋め込みURL)
  3. http://grafana-url:3000/render/d-solo/abcdefg/node-exporter?orgId=1&from=1234&to=5678&panelId=77 (通知URL)

通知用は画像サイズが小さくなり、他アプリ(チャットやメールなど)に送信するのに便利です。Grafanaはパネル1枚の表示に専用のURLが決まる仕様になっているので、外部ツール(プログラム)が処理しやすい作りです。あとPlaywrightで操作用のコードを書く場合、Auto-waiting という機能が便利です。操作のコードを一部だけ省略している場合、既定では30秒ですが操作を待つようになります。その該当箇所を手動で操作すると、そのタイミングで次の操作のコードが自然に実行されるので、完全に作り込まなくても、画面操作をコード化できます。またコードを省略していた部分に pause という関数を書いておくと、Playwright Inspectorを使って任意のタイミングで再開できるようになります。 そして、本文でメイン側(node.js)としている箇所ですが、Java/Python/C#での実装がサポートされています(coreの部分が内部でAPI化され公開されている)。監視チームではPythonをよく用いるのでPythonが利用できるのは良い点です。Grafanaとの相性もよく、活用の範囲が広がりそうです。