シェルのヒストリをKibanaで可視化してみた

# はじめに
いろいろデータ可視化の可能性を探っていると、とても興味を惹かれる記事を見つけました。

 * [Vimでの作業をKibanaで可視化してみた](http://qiita.com/uochan/items/6232d7203cef1ac167ab)

VimのメトリクスをElasticsearchに投入する為にプラグインを作成されていて素直にすごいなぁと思いました。

 * [ブラウザの閲覧履歴で人生の棚卸し（embulk + elasticsearch + kibanaで可視化）](http://qiita.com/maeharin/items/a2fb6242d26555c85dec)

そこに抽出できる履歴データがあれば振り返りたくなりますよね。

昨年11月くらいからデータ可視化のためにEmbulk、Elasticsearch、Kibanaを少し触ってきて、ある程度使い方が分かってきましたので、おさらいを兼ねてシェルのヒストリファイルの可視化をしてみようと思います。

# 必要なもの（環境）

1. そこそこ溜まっている **タイムスタンプ付き** のシェルヒストリファイル
1. Embulk 0.8.8
1. Docker
  1. Elasticsearch 2.2.1
  1. Kibana 4.4.2

試した環境は `Debian GNU/Linux 8.2 (jessie)` です。DockerはElastic環境構築の手間を省くために使用しているので必須ではありません。

# 可視化結果

シェルのヒストリファイルなので実行頻度の高いコマンド程度しか分かりませんが、複数のヒストリファイルを読み込ませることでホスト毎や時間帯での違いが見えて面白いものです。

![シェルのヒストリの可視化結果](https://qiita-image-store.s3.amazonaws.com/0/6124/a1d36c77-22c3-3e41-6eb6-e749fd3b2799.png)

# シェルのヒストリ設定

## zsh

データ可視化にはタイムスタンプが重要なので `extended_history` 必須です。

これを設定するとヒストリファイルは次のように記録されます。

```.zsh_history
: 1421072025:0;ls -l

: 時刻:経過秒;コマンド
```

ちなみに私が使用しているzshのヒストリ関連の設定は以下の通り。ヒストリ行数多めの設定。詳しくはマニュアルを。

 * [zsh: 16 Options - ZSH Documentation](http://zsh.sourceforge.net/Doc/Release/Options.html#History)

```.zshrc
HISTSIZE=1000000
SAVEHIST=$HISTSIZE

setopt extended_history
setopt hist_expire_dups_first
setopt hist_ignore_dups
setopt hist_ignore_space
setopt inc_append_history
setopt share_history

alias history='history -t "%Y-%m-%d %a %H:%M:%S"'
function history-all { history 1 }
```

## bash

`HISTTIMEFORMAT` を指定すると経過時間が出力されるようになります。

```text:.bash_history
#1446886196
ls -l
```

```bash:.bashrc
HISTTIMEFORMAT='%Y-%m-%dT%T%z '
```

 * [Bash Reference Manual: Bash Variables](https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#Bash-Variables)

# Elasticsearch

Javaの環境わからないのでdockerを使って楽します。

```bash
sudo docker run -p 9200:9200 -p 9300:9300 --name es -d elasticsearch
```

## Elasticsearchプラグイン

### elasticsearch-kopf

インデックスのメンテナンスはブラウザで楽をしたいのでkopfプラグインをインストールします。

 * [lmenezes/elasticsearch-kopf](https://github.com/lmenezes/elasticsearch-kopf)
 * [elasticsearch-kopfの紹介（概要） - @johtaniの日記 2nd](http://blog.johtani.info/blog/2014/04/05/intro-elasticsearch-kopf-1/)

```bash
sudo docker exec es /bin/bash -c '/usr/share/elasticsearch/bin/plugin install lmenezes/elasticsearch-kopf'
```

### elasticsearch-head

好みですがheadプラグインでもよいかもです。

 * [ElasticSearch Head](https://mobz.github.io/elasticsearch-head/)

```bash
sudo docker exec es /bin/bash -c '/usr/share/elasticsearch/bin/plugin install mobz/elasticsearch-head'
```

# Kibana

Nodeの環境わからないのでdockerを使って楽します。

```bash
sudo docker run -p 5601:5601 --link es:elasticsearch --name kibana -d kibana
```

Elasticsaerchクラスタのステータスをグリーンにするためだけに、Kibanaが使用するインデックス(`.kibana`)のレプリカ数を0にします。

```bash
curl -XPUT 'localhost:9200/.kibana/_settings' -d '
{
    "index" : {
        "number_of_replicas" : 0
    }
}'
```

## Kibanaプラグイン

### tagcloud

 * [stormpython/tagcloud: Tag Cloud Plugin for Kibana 4](https://github.com/stormpython/tagcloud)

```bash
docker exec kibana /bin/bash -c '/opt/kibana/bin/kibana plugin -i tagcloud -u https://github.com/stormpython/tagcloud/archive/master.zip'
```

### heatmap

 * [stormpython/heatmap: Heatmap Plugin for Kibana 4](https://github.com/stormpython/heatmap)

Kibana 4.3以上向けのアルファ版ですが、試した環境では動作したのでこれを使用します。

```bash
sudo docker exec kibana /bin/bash -c '/opt/kibana/bin/kibana plugin -i heatmap -u https://github.com/stormpython/heatmap/archive/master.zip'
```

### sense

登録したデータの確認などにブラウザで楽をしたいのでsenseを入れます。

 * [Elasticsearch 2.0.0リリース(日本語訳) - @johtaniの日記 2nd](http://blog.johtani.info/blog/2015/10/29/elasticsearch-2-0-0-released-ja/)
 * [The Sense UI](https://www.elastic.co/guide/en/sense/current/sense-ui.html)

```bash
docker exec kibana /bin/bash -c '/opt/kibana/bin/kibana plugin --install elastic/sense'
```

プラグインを読み込ませるためにKibanaを再起動します。

```bash
sudo docker retsart kibana
```

# Embulk

ElasticsearchへのデータローディングツールとしてEmbulkを使います。

 * http://www.embulk.org/

実行にはJavaのランタイムが必要なので今回はOpenJDKをインストールします。

```bash
sudo apt-get install openjdk-7-jdk
```

embulkのインストールは基本的に公式にある手順で行います。

 * [Quick Start - Linux & Mac & BSD](https://github.com/embulk/embulk#linux--mac--bsd)

ダウンロード先がSSLのCDNにリダイレクトされるのでcurlオプションに`--insecure (-k)`を付けておきます。

```bash
curl -k --create-dirs -o ~/.embulk/bin/embulk -L 'http://dl.embulk.org/embulk-latest.jar'
chmod +x ~/.embulk/bin/embulk
```

embulkのパスを永続化する場合は手順通りにしますが、

```bash
echo 'export PATH="$HOME/.embulk/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```

今回は一時的に設定します。

```bash
export PATH=$HOME/.embulk/bin:$PATH
```

## Embulkプラグイン

### embulk-filter_add_time

タイムスタンプの型変換などを行うプラグイン。

 * [treasure-data/embulk-filter-add_time](https://github.com/treasure-data/embulk-filter-add_time)

```bash
embulk gem install embulk-filter-add_time
```

timestamp型をlong型に複製するために使用します。

### embulk-filter-column

カラムの追加、削除を行うプラグイン。

 * [sonots/embulk-filter-column](https://github.com/sonots/embulk-filter-column)

```bash
embulk gem install embulk-filter-column
```

後続のフィルタ用に複数のカラムにコピーするために使用します。

### embulk-filter-eval

Rubyの評価式で値を書き換えるプラグイン。

 * [embulk-filter-eval というフィルタープラグイン書いた - scramble cadenza](http://mgi.hatenablog.com/entry/2015/03/19/084430)
 * [mgi166/embulk-filter-eval](https://github.com/mgi166/embulk-filter-eval)
 * [A-AUTO 50 開発者ブログ: embulk-filter-evalでどんなデータ変換ができるのか試してみる](http://a-auto50.blogspot.jp/2015/06/embulk-filter-eval.html)

タイムスタンプから曜日、時間に変換したり、コマンドラインからコマンドと引数を分割するために使用します。

```bash
embulk gem install embulk-filter-eval
```

### embulk-filter-insert

任意のカラムとして指定した値を追加するプラグイン。

 * [muziyoshiz/embulk-filter-insert](https://github.com/muziyoshiz/embulk-filter-insert)

ホスト名をliquidテンプレートの変数として、どのホストのヒストリファイルであるかを認識させるために使用します。

```bash
embulk gem install embulk-filter-insert
```

### embulk-filter-row

指定した条件でデータをフィルタするプラグイン。

 * [sonots/embulk-filter-row](https://github.com/sonots/embulk-filter-row)

異常値を弾くために使用します。（`grep -v`代わり）

```bash
embulk gem install embulk-filter-row
```

### embulk-filter-split

指定した条件でデータを複数要素に分割するプラグイン。

 * [toyama0919/embulk-filter-split](https://github.com/toyama0919/embulk-filter-split)

セミコロン1行にまとめて実行したコマンドやパイプで繋いで実行したコマンドを分割するために使用します。

```bash
embulk gem install embulk-filter-split
```

### embulk-output-elasticsearch

Elasticserachへの出力プラグイン。

 * [muga/embulk-output-elasticsearch](https://github.com/muga/embulk-output-elasticsearch)

```bash
embulk gem install embulk-output-elasticsearch
```

# データローディング

## インデックステンプレートの登録

今回作成するインデックスの定義をインデックステンプレートとして登録します。

 * [Index Templates](https://www.elastic.co/guide/en/elasticsearch/reference/2.2/indices-templates.html)

定義は以下の通り。

 * Kibanaでの設定を手軽にするためインデックス名は`logstash-command_history`
 * お試し環境なのでレプリカ数は0
 * 全文検索は不要なので`_all`フィールドは使用しない
 * フィールド定義
  * `@timestamp` コマンド実行時刻。Kibanaで識別しやすいようにこの名前を使います
  * `@wday` コマンド実行時刻の曜日(1:月曜〜7:日曜)
  * `@hour` コマンド実行時刻の時間
  * `@host` シェル実行ホスト名
  * `@shell` シェル種別
  * `@command` コマンドライン（Analyzeしないものを`@command.raw`）
  * `command` コマンド名
  * `args` コマンド引数

```bash
curl -XPUT localhost:9200/_template/command_history_template -d'
{
  "template": "logstash-*",
  "settings": { "number_of_replicas" : 0 },
  "mappings": {
    "command_history": {
      "_all": { "enabled": false },
      "properties": {
        "@timestamp": {
          "type": "date"
        },
        "@wday": {
          "type": "byte"
        },
        "@hour": {
          "type": "byte"
        },
        "@host": {
          "index": "not_analyzed",
          "type": "string"
        },
        "@shell": {
          "index": "not_analyzed",
          "type": "string"
        },
        "@command": {
          "type": "string",
          "fields": {
            "raw": {
              "index": "not_analyzed",
              "type": "string"
            }
          }
        },
        "command": {
          "index": "not_analyzed",
          "type": "string"
        },
        "args": {
          "type": "string"
          }
        }
      }
    }
  }
}'
```

## Embulkの設定

Embulkプラグインですべて望みのフォーマットに変換することは難しそうだったので、シェルで加工したものを標準入力として読み込ませる方針です。

 * 入力はTSVを標準入力から
 * 引数でホスト名とシェルを与える
 * タイムスタンプが保持されていないヒストリを除くようにしています。(`1327083640` `2012-01-21 03:20:40`)

```yaml:command_history.yml.liquid
in:
  type: file
  path_prefix: /dev/stdin
  parser:
    type: csv
    delimiter: "\t"
    columns:
    - {name: '@timestamp', type: timestamp, format: '%s'}
    - {name: '@command', type: string}

filters:
  - type: row
    conditions:
      - {column: '@timestamp', operator: ">", argument: "1327083640", format: '%s' }

  - type: insert
    columns:
      - '@host': {{ env.host }}
      - '@shell': {{ env.shell }}
    at: 1

  - type: split
    delimiter: '|'
    keep_input: true
    target_key: '@command'

  - type: add_time
    to_column:
      name: '@wday'
      type: long
    from_column:
      name: '@timestamp'

  - type: column
    add_columns:
      - {name: '@hour', src: '@wday' }
      - {name: command, src: '@command' }
      - {name: args, src: '@command' }

  - type: eval
    eval_columns:
      - '@wday': Time.at(value).strftime('%u')
      - '@hour': Time.at(value).strftime('%H')
      - command: value.sub(/^\s*(?:(?:env )?[^=]+=\S+ )*(\S+).*$/, '\1')
      - args: if value.match(/^\s*\S+ (.*?)\s*$/) then
                value=$1
              else
                value=nil
              end

out:
  type: elasticsearch
  cluster_name: elasticsearch
  nodes:
    - {host: localhost, port: 9300}
  index: logstash-command_history
  index_type: command_history
```

## ヒストリファイル(bash)のローディング

 * awkで時刻文字列とコマンドラインを1行にしてタブ区切りにします
 * `file` に任意の.bash_historyのパス
 * `host` に任意のホスト名

```bash
file=/path/to/bash_history
host=somehost

cat $file | awk '{getline ts; print substr(ts,2)"\t"$0}' |
  host=$host shell=bash embulk run ./command_history.yml.liquid
```

## ヒストリファイル(zsh)のローディング

 * sedでタブ区切りにします（複数行には対応しません）
 * `file` に任意の.zsh_historyのパス
 * `host` に任意のホスト名

```bash
file=/path/to/bash_history
host=somehost

cat $file | cut -b 3- | sed -e 's/:0;/\t/' |
  host=$host shell=zsh embulk run ./command_history.yml.liquid
```

# おわりに

この記事は昨年末に書き始めたのですが、いろいろあって公開が遅れているうちに embulk-output-pluginがElasticsearch 2系に正式対応したり、embulk-filter_add_timeが作成されたり、Elasticsearchへの登録が楽になりました。

また、Kibanaのheatmapプラグインがアルファ版とはいえ動くようになったので、曜日や時間帯の棒グラフを並べなくて済むことを知りました。

そして家で使っているホストに絞ると、土日の不規則な活動時間も明らかですね。。。

![シェルのヒストリの可視化結果(@home)](https://qiita-image-store.s3.amazonaws.com/0/6124/aa5587f6-175a-4233-1940-d28dc41139f2.png)

ビッグデータとは言わず、みなさんも手元にある履歴データをElastic環境で可視化してみると新たな発見があるかも知れません。