難しいLogstashのgrokフィルタを簡単に書く方法

GYAOの窓際エンジニア　玉利です。

GW前にベトナムに行って、GW中（向こうは通常稼働です）の宿題を渡してきました。ちょうど日本でも別件サービスローンチが控えており、結局

- ホーチミンから深夜便で帰国。日本時間02:30発、朝8:30着
- 帰宅後、そのままフルタイムで働く

というエクストリーム勤務になってしまて無事死亡しております。
いちど、エクストリーム出社はやってみたかったのでネタになりました。もうやりません。

![IMG_7923.jpg](https://qiita-image-store.s3.amazonaws.com/0/113049/aaf7f8d4-6369-4e95-a5a0-414352384087.jpeg)

__もしもあなたがジロリアンだったとしても、ベトナムならば安心です。もやしは食べ放題です__

さて、そのエクストリーム勤務になった原因というのが、Apache NiFiのログをElasticsearchに流してキューの数を監視したいという理由があったからです。Logstashには、JavaやRubyの複数行にわたるバックトレースログを取得するような機能がついてます。それはそれでいいのですが、NiFiのログが更に上を行く手強いものでした。以下、格闘の経緯を御覧ください。

# NiFiのstatistics logを拾う

Apache NiFiはGUIでフローを書いて処理ができる素晴らしいものなのですが、GUIものにありがちな欠点として、GUIを開いてないと何が起きてるのかわからない問題があります。プロダクションサービスに突っ込むので、傾向異常を検出したいのですが、それを取る仕掛けはまだ未整備です。

開発者が作ったものと運用者が作ったものの違いは、実際に動かした時に「適切な見える化ができているか？」という場所に顕れてきます。残念ながらNiFiは前者に該当しそうです。

<NIFI LOG>
![nifi-stats.png](https://qiita-image-store.s3.amazonaws.com/0/113049/8762ae67-00ba-c816-81b5-4a299cef5efa.png)


このMS-DOS時代を彷彿とさせるテキスト版統計ログ、しびれます。こんなの、普段からずっと見ていられません。私だったら、最初にダッシュボードに統計ページとアラーティングを仕込みますね。

仕方ないので自分で作ることにして、このできの悪いログを解析します。本当はjsonで書いてくれれば楽なんですけど、百歩譲ってxmlで欲しかったんですけど。

今回ほしいのは、Queueが溜まってしまう問題を検出したいだけなので、一番右の列になります。いつものgrep & awkなら簡単に書けるんですけど、ドキュメント見ながら適当に書いても全然動かない！

Logstashのconfigをいじっていじって、いくつか、拾えるカラムが出来たり消えたりして、これで３時間経過。

## 解析ヘルパーがあった！

頑張って英文資料をさがしてたら、grokの構文構築ヘルパーがありました。これで一気に作業がすすみます。

http://grokconstructor.appspot.com/do/match

使い方は、上のウィンドウにログをペースト、下に構文をペーストしてGoを押すと解析してくれます。

これのおかげで、結構、ノリがわかってきました。以下にプリセットのパターンマッチ用の文法が解説されてます。

https://github.com/elastic/logstash/blob/v1.4.2/patterns/grok-patterns

- | がデリミタになっているので、バックスラッシュでエスケープしてマッチさせる
- IDっぽいものは、UUIDで取れる %{UUID:connection_id}
- スペース一文字ならそのままスペースで問題ないが、スペースで揃えられてる場合は%{SPACE}でマッチさせる


### やっっぱり拾えないのが出てくる。おかしい

これで、最初のテストデータは処理できたのですが、フィールドの値が毎回全部入ってくるとは限らないため旨く取れない問題が続きました。パターンは以下になります。これを一行でバチっとマッチするようにしてやりたい。。のは無理でした。


#### パターン１　全フィールドが埋まってるもの

```
| cf3d527a-fc72-3837-984e-1c82eccf7ea8 | RouteOnAttribute               | methods_get                          | RouteOnAttribute               |       150 / 0 bytes |       150 / 0 bytes |         0 / 0 bytes |
```


#### パターン２　フィールドが抜けてるもの

```
| 669891eb-61e5-3559-a3c0-eee69f49614c | 500_output_port                |                                      | input_port                     |         0 / 0 bytes |         0 / 0 bytes |         0 / 0 bytes |
```

#### パターン３　フィールドのデータが２つあるもの

```
| a4baca1a-8eaa-3f6d-a824-e1dab665e99b | EvaluateJsonPath               | failure, unmatched                   | LogAttribute                   |         0 / 0 bytes |         0 / 0 bytes |         0 / 0 bytes |
```


#### パターン４　フィールド内にスペースがあるもの

```
| f4270d5d-880d-314f-acc3-05dc28b79d14 | InvokeHTTP                     | No Retry, Failure                    | 500_output_port                |         0 / 0 bytes |         0 / 0 bytes |         0 / 0 bytes |
```

## 妥協

結局、こうなりました。各パターン毎にベタ書きです。ダメなエンジニアと呼ばれても仕方ありません。だれかエライ人、いい方法おしえてください。

2016/05/10追記：Queuedの数がstringで保存されてしまってグラフ化できないので、integerに変換する改修を行いました。

```出来上がったLogstash用NiFi.conf
input {
  file {
    path => "/var/log/nifi/nifi-app.log"
    type => "nifi"
  }
}

filter {
  if [type] == "nifi" {


    grok {
      #patterns_dir => "./patterns"

      # NiFiのいけてないログにあわせて、マッチングをベタ書きして拾ってます。ダメなパターンを見っけたら以下でためして追加。
      # https://github.com/elastic/logstash/blob/v1.4.2/patterns/grok-patterns
      # http://grokconstructor.appspot.com/do/match#result
      match => [
        "message", "%{YEAR:year}-%{MONTHNUM:month}-%{MONTHDAY:day} %{TIME:time} %{WORD:level} +%{GREEDYDATA:log_message}",
        "message", "^-----",
        "message", "^\| %{UUID:connection_id} \| %{WORD:source}%{SPACE}\| %{WORD} %{WORD}, %{WORD}%{SPACE}\| %{WORD}%{SPACE}\|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT:queued} / %{INT} %{WORD} \|",
        "message", "^\| %{UUID:connection_id} \| %{WORD:source}%{SPACE}\| %{WORD}%{SPACE}\| %{WORD}%{SPACE}\|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT:queued} / %{INT} %{WORD} \|",
        "message", "^\| %{UUID:connection_id} \| %{WORD:source}%{SPACE}\| %{WORD}, %{WORD}%{SPACE}\| %{WORD}%{SPACE}\|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT:queued} / %{INT} %{WORD} \|",
        "message", "^\| %{UUID:connection_id} \| %{WORD:source}%{SPACE}\|%{SPACE}\| %{WORD}%{SPACE}\|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT} / %{INT} %{WORD} \|%{SPACE}%{INT:queued} / %{INT} %{WORD} \|"
      ]
    }
  }
  mutate {
    convert => {
      queued => integer
    }
  }
}

output {
  elasticsearch { hosts => ["<host01>:9200","<host02>:9200"] }
#  stdout { codec => rubydebug }
}
```