動的に Kibana のダッシュボードを作成する

CloudWatch からダウンロードした RDS インスタンスのメトリクスを、 Elasticsearch に突っ込んで、 Kibana のダッシュボードで可視化するところまではできた。

tnakamura.hatenablog.com

RDS インスタンスは今後増えていく予定で、 Kibana のダッシュボードは RDS インスタンスごとに分けたい。 RDS インスタンスが増えるたびに、Kibana をぽちぽち操作してダッシュボードを作るのは手間だな。

なんとか自動化できないものか調べていたら、 Kibana の設定は Elasticsearch の .kibana インデックスに保存されているみたいだった。 ダッシュボードは type = dashboard に、 Visualization は type = visualization に保存されていた。

ということは、ダッシュボードの JSON を .kibana インデックスにスクリプトで登録すれば、 動的にダッシュボードを追加できそうだ。

一から JSON を書くのは面倒なので、すでに作成したダッシュボードや Visualization もろもろの JSON を Kibana からエクスポートし、加工してテンプレートにした。 かなり長いので、一部抜粋。

[
  {
    "_id": "${instance}",
    "_type": "dashboard",
    "_source": {
      "title": "${instance}",
      "hits": 0,
      "description": "",
      "panelsJSON": "[{\"col\":1,\"id\":\"${instance}-CPUUtilization\",\"panelIndex\":1,\"row\":1,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"col\":4,\"id\":\"${instance}-DatabaseConnections\",\"panelIndex\":2,\"row\":1,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"col\":7,\"id\":\"${instance}-FreeableMemory\",\"panelIndex\":3,\"row\":1,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"col\":10,\"id\":\"${instance}-DiskQueueDepth\",\"panelIndex\":4,\"row\":1,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"col\":1,\"id\":\"${instance}-FreeStorageSpace\",\"panelIndex\":5,\"row\":3,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"col\":4,\"id\":\"${instance}-ReadIOPS\",\"panelIndex\":6,\"row\":3,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"col\":7,\"id\":\"${instance}-WriteIOPS\",\"panelIndex\":7,\"row\":3,\"size_x\":3,\"size_y\":2,\"type\":\"visualization\"},{\"id\":\"${instance}-NetworkReceiveThroughput\",\"type\":\"visualization\",\"panelIndex\":8,\"size_x\":3,\"size_y\":2,\"col\":10,\"row\":3},{\"id\":\"${instance}-NetworkTransmitThroughput\",\"type\":\"visualization\",\"panelIndex\":9,\"size_x\":3,\"size_y\":2,\"col\":1,\"row\":5},{\"id\":\"${instance}-ReadLatency\",\"type\":\"visualization\",\"panelIndex\":10,\"size_x\":3,\"size_y\":2,\"col\":4,\"row\":5},{\"id\":\"${instance}-WriteLatency\",\"type\":\"visualization\",\"panelIndex\":11,\"size_x\":3,\"size_y\":2,\"col\":7,\"row\":5}]",
      "optionsJSON": "{\"darkTheme\":true}",
      "uiStateJSON": "{\"P-1\":{\"vis\":{\"legendOpen\":false}},\"P-2\":{\"vis\":{\"legendOpen\":false}},\"P-3\":{\"vis\":{\"legendOpen\":false}},\"P-4\":{\"vis\":{\"legendOpen\":false}},\"P-5\":{\"vis\":{\"legendOpen\":false}},\"P-7\":{\"vis\":{\"legendOpen\":false},\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}},\"P-8\":{\"vis\":{\"legendOpen\":false}},\"P-11\":{\"vis\":{\"legendOpen\":false}},\"P-10\":{\"vis\":{\"legendOpen\":false}},\"P-9\":{\"vis\":{\"legendOpen\":false}},\"P-6\":{\"vis\":{\"legendOpen\":false}}}",
      "version": 1,
      "timeRestore": false,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"filter\":[{\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\"}}}]}"
      }
    }
  },

  // search と visualization は省略
]

このテンプレートからダッシュボードの JSON を出力する Python スクリプトを書いた。

# -*- coding: utf-8 -*-
import os
import sys
import codecs
from string import Template


TEMPLATE_FILE_NAME = "dashboard_template.json"
JSON_FILE_SUFFIX = "_dashboard.json"


class DashboardGenerator(object):

    def _get_work_dir(self):
        dir_path = os.path.abspath(os.path.dirname(__file__))
        return dir_path

    def _get_template_path(self):
        dir_path = self._get_work_dir()
        template_path = os.path.join(dir_path, TEMPLATE_FILE_NAME)
        return template_path

    def _get_template(self):
        template_path = self._get_template_path()
        with open(template_path, "r") as f:
            template_text = f.read()
            template = Template(template_text)
            return template

    def _generate_dashboard_json(self, rds_instance_name):
        template = self._get_template()
        json = template.substitute(instance=rds_instance_name)
        return json

    def _save_dashboard_json(self, rds_instance_name, json):
        file_name = rds_instance_name + JSON_FILE_SUFFIX
        dir_path = self._get_work_dir()
        file_path = os.path.join(dir_path, file_name)
        with codecs.open(file_path, "w", "utf-8") as f:
            f.write(json)

    def generate(self, rds_instance_name):
        json_data = self._generate_dashboard_json(rds_instance_name)
        self._save_dashboard_json(rds_instance_name, json_data)


if __name__ == "__main__":
    generator = DashboardGenerator()
    generator.generate("rds-0001")

出力した JSON を elasticsearch クライアントを使って .kibana インデックスに登録すれば、 ダッシュボードを追加できる。これも Python スクリプトを書いた。

# -*- coding: utf-8 -*-
import os
import sys
import json
from elasticsearch import Elasticsearch


class KibanaSetup(object):
    def __init__(self):
        self.es = Elasticsearch("localhost:9200")

    def _get_work_dir(self):
        dir_path = os.path.abspath(os.path.dirname(__file__))
        return dir_path

    def _get_dashboard_json_path(self, rds_instance_name):
        work_dir = self._get_work_dir()
        file_path = os.path.join(work_dir, rds_instance_name + "_dashboard.json")
        return file_path

    def _load_dashboard_json(self, rds_instance_name):
        file_path = self._get_dashboard_json_path(rds_instance_name)
        with open(file_path) as f:
            json_data = json.load(f)
            return json_data

    def _post_object(self, obj):
        self.es.index(index=".kibana", doc_type=obj["_type"], id=obj["_id"], body=obj["_source"])

    def _create_dashboard(self, rds_instance_name):
        json_data = self._load_dashboard_json(rds_instance_name)
        for obj in json_data:
            self._post_object(obj)

    def setup(self, rds_instance_name):
        self._create_dashboard(rds_instance_name)


if __name__ == "__main__":
    app = KibanaSetup()
    app.setup("rds-0001")

これでやりたいことはだいたい出来た。 ダッシュボードに Visualization を追加するときはテンプレートをいじる必要があって、 そこはまだ手間だけど当面は増やす予定ないからいいことにしよう。