47
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Kibana 4のプラグインをつくってみよう

Last updated at Posted at 2014-12-07

この投稿は、AngularJS Advent Calendar 2014 (Adventarの方)の7日目の記事です。

はじめに

Kibanaって便利ですよね。
Kibana 4ではElasticsearchのAggregationsに対応したので、より柔軟な表現ができるようになりました。
とは言え、標準で用意されているグラフだけでは表現できないケースがあるのも事実です。

ところで、KibanaはAngularJSでつくられています。
幸いなことにぼくはAngularJSチョットデキルので、Kibanaを拡張することもできるんじゃないだろうか考えていたところ、Kibana 4のソースコードの中にpluginsというディレクトリがあるのを発見しました。

喜び勇んでpluginsディレクトリを開いてみたわけですが、README.txtにはPLEASE DON'T WRITE CUSTOM PLUGINSと書いてあります。どうやらまだプラグインの仕様が定まっていないため、意図的にドキュメントも用意していないようです。そして正式にサポートされる時期も未定なようです。

それでもプラグインつくりたいですよねー。
というわけで、今回紹介する手法は仕様が変わった時に使えなくなる可能性が高いですが、Kibanaプラグインのつくり方を紹介してみたいと思います。

プラグインをつくる

Kibanaのビルド環境

まずはKibanaをビルドする環境を用意します。
Node.jsとRuby 1.9.3とJavaがインストールされていれば、以下の手順でKibanaをビルドして実行することができます。
grunt devの実行時に--with-esを付与すれば、Elasticsearchも一緒に起動してくれます。

$ git clone https://github.com/elasticsearch/kibana.git
$ cd kibana
$ npm install
$ bower install
$ cd src/server
$ bundle
$ cd ../../
$ grunt dev --with-es

CONTRIBUTING.md参照

どんなプラグインをつくろう?

今回はcircle_visというグラフのプラグインをつくることにします。

例えばApacheのaccess.logを解析するときに、リクエストの種類やホストなどでグルーピングした結果を円で囲み、円の大きさでリクエスト数の多さやレスポンスタイムを表現するような可視化をおこなうものを想定します。

描画部分については、D3.jsのサイトのExampleにあるCircle Packingを使わせてもらうと思います。

ファイルの構成

src/kibana/pluginsディレクトリの下にcircle_visというディレクトリを作成し、以下のようなファイルを追加していきます。

kibana
 └─ src
    └─ kibana
       └─ plugins
          └─ circle_vis
             ├─ circle_chart.js          : グラフの描画処理
             ├─ circle_vis.html          : グラフの描画領域のテンプレート
             ├─ circle_vis.js            : グラフの定義を記述したファイル
             ├─ circle_vis.less          : グラフのスタイルを記述
             ├─ circle_vis_controller.js : コントローラ
             ├─ circle_vis_params.html   : このグラフ特有のパラメータを入力する画面のテンプレート
             └─ index.js                 : 最初に読み込まれるファイル

各ファイルの詳細については、以降で説明します。

グラフの定義

まずはindex.jsを作成します。
pluginsディレクトリの下にindex.jsを置いておくと、Kibanaが自動的にロードしてくれる仕組みとなっているようです。

index.js
define(function (require) {
  // 新しいグラフの種類を登録
  require('registry/vis_types').register(function (Private) {
    return Private(require('plugins/circle_vis/circle_vis'));
  });
});

Kibanaはモジュール管理にRequireJSを利用しているので、defineの中に処理を書くようにします。

まず、新しいグラフを追加する場合は、require('registry/vis_types').registerを利用します。この関数にcircle_vis.jsを読み込んだ結果を返す関数を渡します。
なおPrivateは、Kibanaが独自で用意しているサービスで、requireで読み込んだ関数の引数の名前から依存関係を解決して、サービスをインジェクションしてくれます(AngularJSのDIの仕組みを利用)。

circle_vis.jsは次のようになります。

circle_vis.js
define(function (require) {
  // 利用するCSSとControllerを読み込む
  require('css!plugins/circle_vis/circle_vis.css');
  require('plugins/circle_vis/circle_vis_controller');

  return function (Private) {
    var TemplateVisType = Private(require('plugins/vis_types/template/template_vis_type'));
    var Schemas = Private(require('plugins/vis_types/_schemas'));

    return new TemplateVisType({
      // グラフを一意に示す名前
      name: 'circle',
      // 画面に表示される名前
      title: 'Circle',
      // アイコン(Font Awesomeのアイコン名を指定)
      icon: 'fa-circle-o',
      // このグラフのテンプレートファイル
      template: require('text!plugins/circle_vis/circle_vis.html'),
      // このグラフ特有のパラメータ
      params: {
        // パラメータのデフォルト値
        defaults: {
          // 一番外側の円の直径
          diameter: 960
        },
        // パラメータ入力画面(画面左のサイドバーに表示される)
        editor: require('text!plugins/circle_vis/circle_vis_params.html')
      },
      // Elasticsearchに投げるクエリのAggregationsの定義
      schemas: new Schemas([
        {
          // 円のサイズを決めるための項目
          group: 'metrics',
          name: 'metric',
          title: 'Metric',
          min: 2,
          max: 2,
          defaults: [
            { schema: 'metric', type: 'count' }
          ]
        },
        {
          // グルーピングの項目。複数入力可能
          group: 'buckets',
          name: 'segment',
          title: 'Grouping',
          min: 1
        }
      ])
    });
  };
});

まず、利用するCSSとControllerをrequireで読み込みます。ここでCSSを読み込んでおけば、HTMLにCSSを読み込む処理を書く必要はありません。

グラフの定義は、TemplateVisTypeのインスタンスに各種プロパティを設定することでおこないます。
以下、各種プロパティの解説です。

name, title, icon

nameには内部的な名前を、titleには画面に表示される名前を登録します。
iconはFont Awesomeのものが利用できるので http://fortawesome.github.io/Font-Awesome/icons/ から好きなものを選んで、名前を指定します。

template

templateには、このグラフのテンプレートファイルを指定します。
テンプレートファイルは以下のようなものを用意しておきます。
具体的な描画処理はD3.jsを使っておこなうので、ここではコントローラを指定するだけで十分です。

circle_vis.html
<div ng-controller="circleVisController" class="circle-vis">
</div>

params

paramsには、このグラフ特有のパラメータを指定することができます。
ここでは、最も外側の円の直径をピクセル単位で指定できるようにdiameterというパラメータを追加し、入力画面としてcircle_vis_params.htmlを指定します。

circle_vis_params.html
<div class="form-group">
  <label>Diameter</label>
  <input type="range" ng-model="vis.params.diameter" class="form-control" min="320" max="1280" />
</div>

このテンプレートファイルは、画面左側のサイドバーのview optionsの中に表示されることになります。

今回はグラフの一番外側の円の大きさを指定するためのパラメータをスライダーで入力できるようにします。
パラメータは$scope.vis.params.diameterにバインドしておきましょう。

schemas

次にschemasでは、Elasticsearchに投げるAggregationsを入力するための定義を指定します。
Aggregationsとは、検索結果に対して集計処理をおこなうことができる仕組みです。以下のページが分かりやすくて参考になりました。

今回のグラフでは、どの項目でグループ化するのかをbucketsで指定し、円の大きさをmetricsで指定することとします。
グルーピングは入れ子にすることもできるように、複数個の入力が可能なようにします。

コントローラ

つぎにコントローラを定義します。

circle_vis_controller.js
define(function (require) {
  var _ = require('lodash');

  // moduleサービスを取得
  var module = require('modules').get('kibana/circle_vis', ['kibana']);

  // moduleにコントローラを登録
  module.controller('circleVisController', function ($scope, Private) {

    var CircleChart = Private(require('plugins/circle_vis/circle_chart'));
    var chart = new CircleChart();

    // ユーザーが入力したAggregationsの情報を取得
    var metricId = $scope.vis.aggs.bySchemaGroup.metrics[1].id;
    var bucketIds = $scope.vis.aggs.bySchemaGroup.buckets;

    // Elasticsearchのレスポンスの変化を監視
    $scope.$watch('esResponse', function (resp) {
      if (resp) {
        // D3.jsで描画するときに扱いやすいように、Elasticsearchのレスポンスを加工する
        var createChildren = function (data, index) {
          return _.map(data[bucketIds[index].id].buckets, function (v) {
            var child = {'name': v.key};
            if (v[metricId]) child['size'] = v[metricId].value;
            if (bucketIds[index + 1] && v[bucketIds[index + 1].id]) child['children'] = createChildren(v, index + 1);
            return child;
          });
        };
        var res = {
          'name': 'root',
          'children': createChildren(resp.aggregations, 0)
        };

        // グラフの描画をおこなう
        chart.draw(res, $scope.vis.params.diameter);
      }
    });
  });
});

require('modules').getは、angular.moduleとほぼ同等の機能ですが、指定した名前のモジュールが作成済みであれば既存のものを返し、未作成であれば新しく作成したものを返してくれます。便利ですね。

ユーザーが入力したAggregationsの情報は、$scope.vis.aggs.bySchemaGroup.metrics$scope.vis.aggs.bySchemaGroup.bucketsから取得することができます。
Elasticsearchのレスポンスを解析するときに必要なIDをここから取得します。

Elasticsearchのレスポンスは$scope.esResponseに入るので、$scope.$watchを使ってレスポンスを受け取ったときに描画処理が実行されるようにします。
$scope.$watchで登録した関数では、D3.jsで描画するときに扱いやすいようにElasticsearchのレスポンスを加工して、描画処理を呼び出します。パラメータ設定画面で指定した$scope.vis.params.diameterも一緒に渡すようにします。

描画処理

最後はコントローラから呼び出される描画処理です。
D3.jsのサイトのExampleにあるCircle Packingのソースコードとほぼ同じ内容なので、解説は省略します。

circle_chart.js
define(function (require) {
  return function CircleChartFactory(d3, Private) {

    function CircleChart() {
    }

    CircleChart.prototype.draw = function (data, diameter) {
      var format = d3.format(',d');

      var pack = d3.layout.pack()
        .size([diameter - 4, diameter - 4])
        .value(function (d) {
          return d.size;
        });

      d3.select('.circle-vis').selectAll('svg').remove();

      var svg = d3.select('.circle-vis')
        .append('svg')
        .attr('width', diameter)
        .attr('height', diameter)
        .append('g')
        .attr('transform', 'translate(2,2)');

      d3.select(this.frameElement).style('height', diameter + 'px');

      var node = svg.datum(data).selectAll('.node')
        .data(pack.nodes)
        .enter().append('g')
        .attr('class', function (d) {
          return d.children ? 'node' : 'leaf node';
        })
        .attr('transform', function (d) {
          return 'translate(' + d.x + ',' + d.y + ')';
        });

      node.append('title')
        .text(function (d) {
          return d.name + (d.children ? '' : ': ' + format(d.size));
        });

      node.append('circle')
        .attr('class', 'circle')
        .attr('r', function (d) {
          return d.r;
        });

      node.filter(function (d) {
        return !d.children;
      }).append('text')
        .attr('dy', '.3em')
        .style('text-anchor', 'middle')
        .text(function (d) {
          return d.name.substring(0, d.r / 3);
        });
    };

    return CircleChart;
  };
});

スタイルはcssかlessで記述します。

circle_vis.less
.circle-vis {
  .circle {
    opacity: 1;
    fill: rgb(31, 119, 180);
    fill-opacity: .25;
    stroke: rgb(31, 119, 180);
    stroke-width: 1px;
  }

  .leaf .circle {
    fill: #ff7f0e;
    fill-opacity: 1;
  }

  text {
    font: 10px sans-serif;
  }
}

lessで記述した場合は、tasks/config/less.jsのsrcに'<%= plugins %>/circle_vis/circle_vis.less'を追加する必要があります。
gruntを実行するとcircle_vis.lessと同じディレクトリにcircle_vis.cssが生成されます。

動かしてみよう

さっそく実行してみましょう。
Visualizeタブを開き、New VisualizationFrom a new searchを選択すると、Circleというグラフが選択できるようになっています。

select_chart.png

そして、画面左のサイドバーからAggregationsを設定し、Applyボタンを押します。
するとこんな画面が表示されました!成功です!

circle_vis.png

Aggregationsの入力に応じてグルーピングの方法が変化したり、円のサイズが変わったり、Diameterのバーを操作して外側の円のサイズを変化させたりできます。

デバッグ

プラグインをつくろうと思っても、最初はなかなか思い通りに動いてくれないものです。

そんなときは、Visualize画面の下の方にあるボタンを押してみましょう。
すると、グラフの下に以下のような画面が現れます。

vis_debug_spy.png

この画面では、Elasticsearchとやりとりしているリクエストやレスポンスなどの情報をみることができます。

まとめ

プラグインをつくるためには、AngularJSとD3.jsとElasticsearchの知識が必要にはなりますが、比較的プラグインがつくりやすい仕組みが提供されていると感じられました。
はやく正式にサポートされてほしいものですね。

今回作ったプラグインのソースはこちら。

エラーケースをあまり考えていないので、実用しようと思うとまだまだ手を加える必要はありますが、プラグインをつくる際の参考にしてもらえればと思います。

47
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?