Kibanaプラットフォームでアプリケーションをつくってみよう

# はじめに
「[Kibana 4のプラグインをつくってみよう](http://qiita.com/zoetro/items/520f01e61833214d0a01)」という記事を書いてから1年が経過しました。
この記事を書いた時点ではKibanaのプラグインシステムは公式にサポートされたものではありませんでしたが、先日Kibana 4.2がリリースされ、ドキュメントにプラグインについて記述されていました。

* [Kibana User Guide [4.2] » Kibana Plugins](https://www.elastic.co/guide/en/kibana/4.2/kibana-plugins.html)

これでようやく公式にプラグインがつくれるぜ！と思ったわけですが、よく調べてみるとなにやら違和感があります。
どうやらこれは、Kibanaにちょっとした機能を追加するようなプラグインではなくて、Kibanaプラットフォーム上に新たなアプリケーションをつくるための仕組みのようです。
バックエンドにNode.jsとElasticsearch、フロントエンドにAngularJSをつかったアプリケーションが、簡単につくれるようになっています。

思っていたものとはちょっと違ったのですが、せっかくなのでプラグインのつくり方を解説したいと思います。

ちなみに、現在公開されているKibanaプラグインには以下のようなものがあります。

* [Marvel](https://www.elastic.co/products/marvel)
* [Sense](https://github.com/elastic/sense)
* [Timelion](https://github.com/elastic/timelion)

Timelionは昨日@johtaniさんが「[Timelionの紹介 - Elasticsearch Advent Calendar 2015 1日目](http://blog.johtani.info/blog/2015/12/01/introduction-timelion/)」で紹介されていますね。

# プラグインの実装準備

それではさっそくプラグインをつくっていきましょう。
なお、Node.js、Kibana 4.2以降、Elasticsearch 2.0以降はすでインストールされているものとします。

## プラグインのひな形をつくる

まずはプラグインのひな形をつくりましょう。
今回は [Kibana Plugin Yeoman Generator](https://github.com/elastic/generator-kibana-plugin) を利用します。

次のコマンドでYeomanとKibana Plugin Yeoman Generatorをインストールします。

~~~sh
npm install -g yo
# 本記事執筆時点では、npmに上がっているgenerator-kibana-pluginが古かったので、githubからインストールします
npm install -g https://github.com/elastic/generator-kibana-plugin.git
~~~

次のコマンドでソースコードのひな形を生成します。

~~~sh
mkdir my-kibana-plugin
cd my-kibana-plugin
yo kibana-plugin
~~~

プラグイン名と説明を入力すると、以下のようなファイルが生成されます。
ここでは`my-kibana-plugin`という名前にしています。

~~~
my-kibana-plugin
├── gulpfile.js
├── index.js
├── package.json
├── public
│   ├── app.js
│   ├── less
│   │   └── main.less
│   └── templates
│       └── index.html
└── server
    └── routes
        └── example.js
~~~

クライアントサイドのコードが`public`の下に、サーバーサイドのコードが`server`の下にあります。

## プラグインのビルド

生成されたコードをビルドしてパッケージをつくります。

~~~sh
npm install
npm run package
~~~

Kibanaのインストールディレクトリの下で次のコマンドを実行すると、プラグインをインストールすることができます。`-i`オプションでプラグイン名、`-u`オプションでビルドしたパッケージのパスを指定してください。

~~~
./bin/kibana plugin -i my-kibana-plugin -u file:///path/to/my-kibana-plugin/target/my-kibana-plugin-0.1.0.tar.gz
~~~

Kibanaを起動します。

~~~
./bin/kibana
~~~

ブラウザからKibanaの画面を開くと、ナビゲーションバーの一番右に見慣れぬアイコンが表示されているので、これをクリックします。

![kibana-plugin-header.png](https://qiita-image-store.s3.amazonaws.com/0/3489/235c0fa5-ab3c-ab93-5d87-6dfc463af174.png)

プラグインのアイコンをクリックして以下のような画面が表示されれば成功です。

![kibana-plugin-first-screen.png](https://qiita-image-store.s3.amazonaws.com/0/3489/aa1e7e49-a033-5ce3-ed3a-fef689bbb0eb.png)

## 開発モード

プラグインの開発時に、ソースコードを少し修正するたびにビルドして、Kibanaを再起動するのはとても面倒です。
そういうときはKibanaの開発モードを利用しましょう。

Kibanaの開発モードは、公式サイトからダウンロードできるものでは利用することができず、ソースコードからKibanaをビルドする必要があります。
[CONTRIBUTING.md](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md)を参考にしてKibanaをセットアップしてください。なお、ビルドのスピード向上のため、`kibana.dev.yml`を設定しておくことをおすすめします。

Kibanaを開発モードで利用するには、以下のように`--dev`オプションを指定します。

~~~sh
./bin/kibana --dev
~~~

つづいて、Kibanaのインストールディレクトリと同じ階層にプラグインのディレクトリを置きます。

~~~
├── kibana
└── my-kibana-plugin
~~~

プラグインのディレクトリに移動して、ビルドをおこないます。

~~~sh
cd my-kibana-plugin
npm start
~~~

これで、プラグインのソースコードを変更すると、自動的に`kibana/installedPlugins/`にプラグインが配備され、さらにKibanaの再起動がおこなわれます。

# プラグインの実装

最近、スコットランド出身の同僚が「ぼくはゲームで日本語おぼえたよ。英語もゲームやってればすぐおぼえるでしょ。」というので、英語のゲームにチャレンジしていたのですが、英単語を調べるのにすごく時間がかかってしまいまともにゲームが進められません。
というわけで、ゲーム内の英語テキストと日本語化MODのテキストをElasticsearchに突っ込んでおいて、英語のテキストを入力したら対応する日本語のテキストを表示するだけのシンプルなアプリをつくってみました。

https://github.com/zoetrope/elastic-translator

これを題材にして、Kibanaプラグインのつくり方を解説していきます。

## index.js

まずは`index.js`を見てみましょう。

~~~index.js
module.exports = function (kibana) {
  return new kibana.Plugin({

    name: 'elastic-translator',
    require: ['kibana', 'elasticsearch'],
    uiExports: {
      app: {
        title: 'Elastic Translator',
        description: 'Sample for Kibana plugin',
        main: 'plugins/elastic-translator/app',
        injectVars: function (server, options) {
          var config = server.config();
          return {
            kbnIndex: config.get('kibana.index'),
            esApiVersion: config.get('elasticsearch.apiVersion'),
            esShardTimeout: config.get('elasticsearch.shardTimeout')
          };
        }
      }
    },
    init: function (server, options) {
      var searchApi = require('./server/api/search');
      searchApi(server);
    }
  });
};
~~~

このコードでは、プラグインの各種設定を記述します。
`Kibana.Plugin`のコンストラクタに渡している`uiExports.app.main`には、クライアントサイドのコードのパスを指定します。
`init`メソッドではサーバーサイドのコードの初期化をおこないます。

## サーバーサイドコード

つづいて、サーバーサイドのコードは以下のようになります。

~~~server/api/search.js
module.exports = function (server) {

  var client = server.plugins.elasticsearch.client;

  server.route({
    path: '/elastic-translator/api/search',
    method: 'GET',
    handler: function (req, reply) {
      client.search({
        index: 'elastic-translator',
        q: 'default_text_en:"' + req.query.keywords + '"'
      }, function (error, response) {
        if (!error) {
          reply(response.hits.hits);
        } else {
          server.log(['error', 'elastic-translator'], error);
          reply(error);
        }
      });
    }
  });
};
~~~

### API

Kibanaでは、Webアプリケーションフレームワークとして [hapi.js](http://hapijs.com/) をつかっています。

上記のコードの中で`server.route`が`hapi.js`のAPIになります。
ここでは、`http://localhost:5601/elastic-translator/api/search`に`GET`リクエストを投げたときのAPIを定義しています。

詳しくは以下のドキュメントを参考にしてください。

* [hapi.js API Reference](http://hapijs.com/api)

### Elasticsearch

`server.plugins.elasticsearch.client`というオブジェクトをつかうと、Kibanaで利用しているElasticsearchにアクセスすることができます。

上記のコードでは`client.search`メソッドを利用して、`elastic-translator`というインデックスの`default_text_en`フィールドにkeywordsで指定した文字列が含まれるドキュメントを検索して返しています。

Elasticsearch Clientの詳細については、以下のドキュメントを参考にしてください。

* https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html


### ログを出力する

ログを出力するには、以下のように`server.log`メソッドにログレベル、プラグイン名、ログメッセージを渡します。

~~~js
server.log(['info', 'my-kibana-plugin'], 'log message');
~~~

Kibanaのログに以下のように出力されます。

~~~
server    log   [21:46:31.321] [info][my-kibana-plugin] log message
~~~

## クライアントサイドコード

最後にクライアントサイドのコードを見てみましょう。

~~~public/app.js
require('plugins/elastic-translator/less/main.less');

require('ui/chrome').setNavBackground('#222222').setTabs([
  {id: 'search', title: 'Search'},
  {id: 'setting', title: 'Setting'}
]);

var app = require('ui/modules').get('app/elastic-translator', []);

require('ui/routes')
  .when('/search', {
    template: require('plugins/elastic-translator/templates/search.html')
  })
  .when('/setting', {
    template: require('plugins/elastic-translator/templates/setting.html')
  })
  .otherwise({
    redirectTo: '/search'
  });

app.controller('searchController', function ($scope, $http) {
  $scope.search = function (keywords) {
    $http.get('/elastic-translator/api/search', {params: {keywords: keywords}})
      .then(function (response) {
        $scope.result = response;
      });
  };
});
~~~

### ナビゲーションバーの設定

以下のコードでは、ナビゲーションバーの設定をおこなっています。
ナビゲーションバーの背景色の指定、アイコンの指定、タブの定義やデフォルトタブの指定などをおこなうことができます。
ここでは、`Search`と`Setting`の2つのタブを用意しています。

~~~js
require('ui/chrome').setNavBackground('#222222').setTabs([
  {id: 'search', title: 'Search'},
  {id: 'setting', title: 'Setting'}
]);
~~~

### ルーティング

クライアントサイドのルーティングには [ngRoute](https://docs.angularjs.org/api/ngRoute/service/$route) を使っています。
以下のコードでは、`search`と`setting`にアクセスしたときのルーティングを指定しています。

~~~js
require('ui/routes')
  .when('/search', {
    template: require('plugins/elastic-translator/templates/search.html')
  })
  .when('/setting', {
    template: require('plugins/elastic-translator/templates/setting.html')
  })
  .otherwise({
    redirectTo: '/search'
  });
~~~

### APIの呼び出し

最後に、先ほどサーバーサイドのコードで定義したAPIを呼び出します。

まず、以下のようなテンプレートを用意します。
検索キーワードを入力するための`input`タグと、検索結果を羅列しているだけのシンプルなものです。
詳細についてはAngularJSのドキュメントを参照してください。

~~~public/templates/search.html
<div class="container" ng-controller="searchController">
  <form role="form" class="search-form" ng-submit="search(keywords)" name="searchKeywords">
    <div class="input-group">
      <input ng-model="keywords" placeholder="Keywords ..." type="text" class="form-control">
      <span class="input-group-btn">
        <button type="submit" >
          <span class="fa fa-play"></span></button>
      </span>
    </div>
  </form>

  <div class="row">
    <div class="col-12-sm">
      <div ng-repeat="val in result.data">
        <div class="panel panel-default">
          <div class="panel-heading">
            {{val._source.default_text_en}}
          </div>
          <div class="panel-body">
            {{val._source.default_text_ja}}
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
~~~

以下のコードでは、AngularJSの [controller](https://docs.angularjs.org/guide/controller) を定義して、その中で [$http](https://docs.angularjs.org/api/ng/service/$http) をつかってサーバーにリクエストを投げています。

~~~js
app.controller('searchController', function ($scope, $http) {
  $scope.search = function (keywords) {
    $http.get('/elastic-translator/api/search', {params: {keywords: keywords}})
      .then(function (response) {
        $scope.result = response;
      });
  };
});
~~~

# まとめ

後半はちょっと駆け足気味になってしまいましたが、いかがだったでしょうか。
個人的な感想としては、Elasticsearchを使ったWebアプリケーションがさくっとお手軽につくれるのがいいなと思いました。

また、今回は紹介しきれませんでしたが、アプリケーション内でKibanaが提供しているコンポーネントを利用することもできるようです。
グラフのコンポーネントを使いまわすことができれば、いろいろと面白いアプリケーションがつくれそうで期待が膨らみますね！