柔軟にログをパースできるようBNFパーサーを実装してみる

前回ログファイルをjsonに変換した後Embulk経由でElasticsearchに取り込むのを試したのですが、ログの出力内容毎に対してパーサーを定義していなかったので、不要なものまで取り込まれていました。 steavevaivai.hatenablog.com

今回は必要なものだけを取り込めるように、またScalaを知らなくてもパーサーを定義できるように外部で宣言できるようにしたいと思います。

調べてみたところパーサジェネレータを使えば受け取った構文規則をもとにパーサーを出力するということでYaccが有名なようで、パーサジェネレータを使えばちょうどやりたいことが出来そうです。

ただせっかくなのでYaccなどの既存のものを使わずに、パーサジェネレータを自作して使ってみたいと思います。構文規則の文法はバッカス・ナウア記法PEGがあるようで、PEGには文脈自由文法にある曖昧さがないらしいですが、バッカス・ナウア記法、正しくはは(拡張バッカス・ナウア記法)https://ja.wikipedia.org/wiki/EBNFに従って実装しました(PEGを知ったのがそのあとのため)

実際の実装は以下になります。 github.com

全ての拡張バッカス・ナウア記法の文法に対応しているわけではありませんが以下には対応しています。

固定文字

root ::= "Hello"
↑のように定義するとrootというシンボルは"Hello"という文字列をパースできるようになります。

繰り返し(1回以上)

root ::= "Hello" +
↑のように定義するとrootというシンボルは"Hello"や"HelloHello"という文字列をパースできるようになります。

繰り返し(0回以上)

root ::= "Hello" *
↑のように定義するとrootというシンボルは空文字列や"Hello"、"HelloHello"という文字列をパースできるようになります。

繰り返し(0回以上)

root ::= "Hello" *
↑のように定義するとrootというシンボルは空文字列や"Hello"、"HelloHello"という文字列をパースできるようになります。

グループ

root ::= ( "Hello" "World") *
()で囲むとグループ化することができ↑のように定義するとrootというシンボルで"HelloWorld"という文字列の0回以上の繰り返しをパースできるようになります。

オプション

root ::= "Hello" ["World"]
[]で囲むとオプションとして扱われ↑のように定義するとrootというシンボルで"Hello"または"HelloWorld"という文字列の0回以上の繰り返しをパースできるようになります。

繰り返し(グループ)

root ::= { "Hello" "World" }
{}で囲むと繰り返しをパースでき↑のように定義するとrootというシンボルで"HelloWorld"という文字列の0回以上の繰り返しをパースできるようになります。

OR

root ::= ("Hello" "World") | ("こんにちは" "世界")
|でor条件を定義でき↑のように定義すると"HelloWorld"または"こんにちは世界"という文字列をパースできるようになります。

これに加えてスペースとスペース以外をパースするためのシンボルsp, notspを初期のシンボルとして利用できるようにしておきました。

では以下のログのパーサーを出力するためのbnfを定義してみたいと思います。

2020-05-24 11:25:18,784 INFO jp.co.teruuu.LoggingService [main] add BAA:99.674
2020-05-24 11:25:18,784 INFO jp.co.teruuu.LoggingService [main] set BBB:5.730
2020-05-24 11:25:18,785 INFO jp.co.teruuu.LoggingService [main] hello hello CCC
2020-05-24 11:25:18,785 INFO jp.co.teruuu.LoggingService [main] move from ABCtoCAC : 68.080
2020-05-24 11:25:18,785 INFO jp.co.teruuu.LoggingService [main] add ACC:20.390
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello hello ABA!
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] add AAA:54.581
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] globalRemove 46.694
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello world AAC
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello hello BCB!
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello hello ABB!
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] set AAC:59.363
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] globalAdd 63.874
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] set CCC:85.101
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] hello hello ABC!
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] hello hello CBC!
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] hello world ACC!
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] globalAdd 55.108
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] move from BBBtoBAC : 46.040
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] move from CAAtoCCC : 90.571
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] remove BCA:36.741

まず以下のadd, remove, setのログについて

2020-05-24 11:25:18,784 INFO jp.co.teruuu.LoggingService [main] add BAA:99.674
2020-05-24 11:25:18,784 INFO jp.co.teruuu.LoggingService [main] set BBB:5.730
2020-05-24 11:25:18,785 INFO jp.co.teruuu.LoggingService [main] add ACC:20.390
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] add AAA:54.581
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] set AAC:59.363
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] set CCC:85.101
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] remove BCA:36.741

bnfでの構文規則は以下のようになりました。適当に出力したログを使っているのですがBAA、BBBの部分はAまたはBまたはCを3つ繋げたもの担っています。

number ::= ("0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9")
date ::= number + "-" number + "-" number + sp number + ":" number + ":" number + "," number +
level ::= ("DEBUG"|"INFO"|"WARN"|"ERROR"|"FATAL")
package ::= notsp
thread ::= notsp
command ::= ("add"|"remove"|"set")
name ::= ("A"|"B"|"C") ("A"|"B"|"C") ("A"|"B"|"C")
point ::= number + "." number +
basiccommand ::= date sp level sp package sp thread sp command sp name ":" point

つぎに以下のログについて

2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] globalRemove 46.694
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] globalAdd 63.874
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] globalAdd 55.108

bnfでの構文規則は以下のようになりました。

number ::= ("0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9")
date ::= number + "-" number + "-" number + sp number + ":" number + ":" number + "," number +
level ::= ("DEBUG"|"INFO"|"WARN"|"ERROR"|"FATAL")
package ::= notsp
thread ::= notsp
global ::= ("globalAdd"|"globalRemove")
name ::= ("A"|"B"|"C") ("A"|"B"|"C") ("A"|"B"|"C")
point ::= number + "." number +
globalcommand ::= date sp level sp package sp thread sp global sp point

それから以下のログについて

2020-05-24 11:25:18,785 INFO jp.co.teruuu.LoggingService [main] move from ABCtoCAC : 68.080
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] move from BBBtoBAC : 46.040
2020-05-24 11:25:18,788 INFO jp.co.teruuu.LoggingService [main] move from CAAtoCCC : 90.571

bnfでの構文規則は以下のようになりました。

number ::= ("0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9")
date ::= number + "-" number + "-" number + sp number + ":" number + ":" number + "," number +
level ::= ("DEBUG"|"INFO"|"WARN"|"ERROR"|"FATAL")
package ::= notsp
thread ::= notsp
name ::= ("A"|"B"|"C") ("A"|"B"|"C") ("A"|"B"|"C")
point ::= number + "." number +
movecommand ::= date sp level sp package sp thread sp "move" sp "from" sp name sp "to" sp name sp ":" sp point

以下のようなhello 〇〇といったログもあるのですが、不要としてbnfは定義しません

2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello hello ABA!
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello world AAC
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello hello BCB!
2020-05-24 11:25:18,786 INFO jp.co.teruuu.LoggingService [main] hello hello ABB!
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] hello hello ABC!
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] hello hello CBC!
2020-05-24 11:25:18,787 INFO jp.co.teruuu.LoggingService [main] hello world ACC!

全体をまとめるとbnfでの構文規則は以下のようになりました。シンボルを複数定義していますが今回の自分の実装ではrootというシンボルでのみパースをしているのでorの条件で繋げています。

number ::= ("0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9")
date ::= number + "-" number + "-" number + sp number + ":" number + ":" number + "," number +
level ::= ("DEBUG"|"INFO"|"WARN"|"ERROR"|"FATAL")
package ::= notsp
thread ::= notsp
command ::= ("add"|"remove"|"set")
global ::= ("globalAdd"|"globalRemove")
name ::= ("A"|"B"|"C") ("A"|"B"|"C") ("A"|"B"|"C")
point ::= number + "." number +
basiccommand ::= date sp level sp package sp thread sp command sp name ":" point
globalcommand ::= date sp level sp package sp thread sp global sp point
movecommand ::= date sp level sp package sp thread sp "move" sp "from" sp name sp "to" sp name sp ":" sp point
root ::= basiccommand | globalcommand | movecommand

確認のためパース対象の文字列と結果を表示したら以下のようになり、想定通りhello 〇〇にのみ失敗しうまくパースできていることが確認できました。

parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] globalRemove 12.938
  ⇒ result: Success(2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] globalRemove 12.938)
parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] hello hello BCC!
  ⇒ result: Failure(Location(1,65),parse failed: expected:`move` actual:`h`)
parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] add CAA:21.538
  ⇒ result: Success(2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] add CAA:21.538)
parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] globalAdd 43.196
  ⇒ result: Success(2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] globalAdd 43.196)
parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] move from CAB to CAA : 17.383
  ⇒ result: Success(2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] move from CAB to CAA : 17.383)
parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] hello hello BAA
  ⇒ result: Failure(Location(1,65),parse failed: expected:`move` actual:`h`)
parse: 2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] globalAdd 8.308
  ⇒ result: Success(2020-05-24 11:30:37,202 INFO jp.co.teruuu.LoggingService [main] globalAdd 8.308)
parse: 2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] hello hello BAA!
  ⇒ result: Failure(Location(1,65),parse failed: expected:`move` actual:`h`)
parse: 2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] globalRemove 61.999
  ⇒ result: Success(2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] globalRemove 61.999)
parse: 2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] move from BCB to BCC : 96.230
  ⇒ result: Success(2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] move from BCB to BCC : 96.230)
parse: 2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] hello hello CAA!
  ⇒ result: Failure(Location(1,65),parse failed: expected:`move` actual:`h`)
parse: 2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] remove BBA:41.788
  ⇒ result: Success(2020-05-24 11:30:37,203 INFO jp.co.teruuu.LoggingService [main] remove BBA:41.788)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] globalAdd 89.201
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] globalAdd 89.201)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] remove CCA:90.795
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] remove CCA:90.795)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] globalAdd 45.250
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] globalAdd 45.250)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] globalAdd 23.735
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] globalAdd 23.735)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] move from ACB to CBA : 77.454
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] move from ACB to CBA : 77.454)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] hello world ACB
  ⇒ result: Failure(Location(1,65),parse failed: expected:`move` actual:`h`)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] set CCC:25.078
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] set CCC:25.078)
parse: 2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] move from CAC to CBC : 83.523
  ⇒ result: Success(2020-05-24 11:30:37,204 INFO jp.co.teruuu.LoggingService [main] move from CAC to CBC : 83.523)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from BCC to CCA : 86.934
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from BCC to CCA : 86.934)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from BBA to AAC : 89.417
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from BBA to AAC : 89.417)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] add CCC:77.076
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] add CCC:77.076)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from CAC to BCC : 76.503
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from CAC to BCC : 76.503)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from BBA to BBA : 69.673
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] move from BBA to BBA : 69.673)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] hello world BAA
  ⇒ result: Failure(Location(1,65),parse failed: expected:`move` actual:`h`)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] set AAC:76.516
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] set AAC:76.516)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] globalRemove 21.810
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] globalRemove 21.810)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] globalRemove 57.912
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] globalRemove 57.912)
parse: 2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] remove CCA:29.730
  ⇒ result: Success(2020-05-24 11:30:37,205 INFO jp.co.teruuu.LoggingService [main] remove CCA:29.730)
parse: 2020-05-24 11:30:37,206 INFO jp.co.teruuu.LoggingService [main] globalAdd 33.462
  ⇒ result: Success(2020-05-24 11:30:37,206 INFO jp.co.teruuu.LoggingService [main] globalAdd 33.462)

パースはできましたが、依然パースできた文字列全体を表示しているだけで不便なので次の段階としてはフォーマットしての表示を目指そうかと思います