この投稿は、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
どんなプラグインをつくろう?
今回は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が自動的にロードしてくれる仕組みとなっているようです。
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
は次のようになります。
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を使っておこなうので、ここではコントローラを指定するだけで十分です。
<div ng-controller="circleVisController" class="circle-vis">
</div>
params
params
には、このグラフ特有のパラメータを指定することができます。
ここでは、最も外側の円の直径をピクセル単位で指定できるようにdiameter
というパラメータを追加し、入力画面として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
で指定することとします。
グルーピングは入れ子にすることもできるように、複数個の入力が可能なようにします。
コントローラ
つぎにコントローラを定義します。
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のソースコードとほぼ同じ内容なので、解説は省略します。
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 {
.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 Visualization
→ From a new search
を選択すると、Circle
というグラフが選択できるようになっています。
そして、画面左のサイドバーからAggregationsを設定し、Applyボタンを押します。
するとこんな画面が表示されました!成功です!
Aggregationsの入力に応じてグルーピングの方法が変化したり、円のサイズが変わったり、Diameterのバーを操作して外側の円のサイズを変化させたりできます。
デバッグ
プラグインをつくろうと思っても、最初はなかなか思い通りに動いてくれないものです。
そんなときは、Visualize画面の下の方にある∧
ボタンを押してみましょう。
すると、グラフの下に以下のような画面が現れます。
この画面では、Elasticsearchとやりとりしているリクエストやレスポンスなどの情報をみることができます。
まとめ
プラグインをつくるためには、AngularJSとD3.jsとElasticsearchの知識が必要にはなりますが、比較的プラグインがつくりやすい仕組みが提供されていると感じられました。
はやく正式にサポートされてほしいものですね。
今回作ったプラグインのソースはこちら。
エラーケースをあまり考えていないので、実用しようと思うとまだまだ手を加える必要はありますが、プラグインをつくる際の参考にしてもらえればと思います。