こちらのエントリを見て、EmbulkでApacheのログをパースするのにgrokというものがあることを知りまして。
EmbulkでアクセスログをLogstash風に取り込む - 見習いプログラミング日記
grok自体、知らなかったです…。
正規表現がベースになっているようですが、パターンに名前を付けて繰り返し利用できるところがポイントみたいです。
なお、自分が前にEmbulkでApacheログをパースした時は、embulk-parser-grokではなくてembulk-parser-apache-custom-logを使っていました。
https://github.com/arielnetworks/embulk-parser-grok
https://github.com/jami-i/embulk-parser-apache-custom-log
このembulk-parser-grokを見ていると、grokというものが単体で使えそうだったので、ちょっとメモとして。
なお、この流れでLogstashでgrokが使えることも知りました…。
準備
まずは、Maven依存関係。
<dependency> <groupId>io.thekraken</groupId> <artifactId>grok</artifactId> <version>0.1.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.3.0</version> <scope>test</scope> </dependency>
テストコードとして、JUnitとAssertJも使います。
テストコードの雛形
以降のコードでは、以下のテストクラス内で書いているものとします。
src/test/java/org/littlewings/grok/GrokTest.java
package org.littlewings.grok; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Map; import com.google.code.regexp.Matcher; import oi.thekraken.grok.api.Grok; import oi.thekraken.grok.api.Match; import oi.thekraken.grok.api.exception.GrokException; import org.assertj.core.data.MapEntry; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class GrokTest { // ここに、テストコードを書く! }
はじめてのGrok for Java
とりあえず、このあたりを見ながら写経的に。
書いたコードは、こちら。
@Test public void gettingStarted() throws GrokException { Grok grok = new Grok(); // パターン登録 grok.addPattern("numeric", "\\d+"); grok.addPattern("alphabetical", "[a-zA-Z]+"); // パターンの組み合わせで、キャプチャするためのパターンをコンパイル grok.compile("%{numeric:n} %{alphabetical:a}"); // 対象の文字列とマッチ Match match = grok.match("12345 abcde"); match.captures(); assertThat(match.isNull()) .isFalse(); // Mapとして取得可能 Map<String, Object> map = match.toMap(); assertThat(map) .containsOnly(MapEntry.entry("n", 12345), MapEntry.entry("a", "abcde")); assertThat(match.toJson()) .isEqualTo("{\"a\":\"abcde\",\"n\":12345}"); Matcher matcher = match.getMatch(); assertThat(matcher.group(1)) .isEqualTo("12345"); assertThat(matcher.group(2)) .isEqualTo("abcde"); }
まず、Grokのインスタンスを生成します。
Grok grok = new Grok();
ここに、正規表現と対応する名前を登録します。今回は、「numeric」と「alphabetical」という2つの名前を登録しました。
// パターン登録 grok.addPattern("numeric", "\\d+"); grok.addPattern("alphabetical", "[a-zA-Z]+");
で、さらにこのパターンを元にしたパターンを作ります。
// パターンの組み合わせで、キャプチャするためのパターンをコンパイル grok.compile("%{numeric:n} %{alphabetical:a}");
numericに対して「n」、alphabeticalに対して「a」という名前でパターンを作成し、コンパイル。
これで、テスト対象の文字列とマッチをかけます。
// 対象の文字列とマッチ Match match = grok.match("12345 abcde");
Matchクラスのインスタンスが返ってくるので、capturesを呼び出しておきます。
match.captures();
これを忘れると、痛い目を見ます…。
パターンにマッチできている場合は、Match#isNullがfalseになるようです。
assertThat(match.isNull()) .isFalse();
キャプチャした結果は、Mapとして取得できます。この時、先ほど作成したパターンに与えた名前(「n」とか「a」)がキーになっています。
// Mapとして取得可能 Map<String, Object> map = match.toMap(); assertThat(map) .containsOnly(MapEntry.entry("n", 12345), MapEntry.entry("a", "abcde"));
数字がintになっているあたり、ある程度型を判別してくれてそうですね。ってコード見たら、intだけっぽい雰囲気もありますが。
JSON形式の文字列としても、取得できます。
assertThat(match.toJson()) .isEqualTo("{\"a\":\"abcde\",\"n\":12345}");
また、内部的に使用している正規表現ライブラリのMatcherを引っこ抜くこともできるようです。使いはしない気はしますけど。
Matcher matcher = match.getMatch(); assertThat(matcher.group(1)) .isEqualTo("12345"); assertThat(matcher.group(2)) .isEqualTo("abcde");
使われているのは、これみたいですね。
文字列がパターンにマッチしない場合
先にも近いことを書きましたが、パターンにマッチしない場合はMatch#isNullがtrueになります。
@Test public void notMatching() throws GrokException { Grok grok = new Grok(); // パターン登録 grok.addPattern("numeric", "\\d+"); grok.addPattern("alphabetical", "[a-zA-Z]+"); // パターンの組み合わせで、キャプチャするためのパターンをコンパイル grok.compile("%{numeric:n} %{alphabetical:a}"); // 対象の文字列とマッチ Match match = grok.match("abcde 12345"); match.captures(); // マッチしなかった場合は、Match#isNullがtrueになる assertThat(match.isNull()) .isTrue(); assertThat(match.toMap()) .isEmpty(); }
Logstashのgrokパターンを使う
Grok for Javaでは、Logstashのgrokパターンが使えます。
Grok filter plugin | Logstash Reference [6.4] | Elastic
というか、そもそも
Grok is inspired by the logstash inteceptor or filter available here
https://github.com/thekrakken/java-grok
って書かれているくらいですからね。
Grok#addPatternFromFileやGrok#addPatternFromReaderを使うことで、ファイルやReaderでパターンを読み込んでGrokに登録することができます。
内部的には、Grok#addPatternを繰り返し呼び出している感じですが。
では、LogstashのgrokパターンをGitHubからダウンロードして使ってみます。
書いたコードは、こんな感じ。
@Test public void patternFromReader() throws IOException, GrokException { Grok grok = new Grok(); HttpURLConnection conn = (HttpURLConnection) new URL("https://raw.githubusercontent.com/logstash-plugins/logstash-patterns-core/v2.0.2/patterns/grok-patterns") .openConnection(); grok .addPatternFromReader( new InputStreamReader( conn.getInputStream(), StandardCharsets.UTF_8)); conn.disconnect(); grok.compile("%{COMBINEDAPACHELOG}"); Match match = grok.match("172.17.0.1 - - [16/Feb/2016:14:30:07 +0000] \"GET http://d.hatena.ne.jp/Kazuhira/20160214/1455460595 HTTP/1.1\" 200 52945 \"http://d.hatena.ne.jp/Kazuhira/\" \"Wget/1.15 (linux-gnu)\""); match.captures(); Map<String, Object> map = match.toMap(); assertThat(map) .contains(MapEntry.entry("clientip", "172.17.0.1"), MapEntry.entry("request", "http://d.hatena.ne.jp/Kazuhira/20160214/1455460595"), MapEntry.entry("agent", "Wget/1.15 (linux-gnu)")); }
パターンファイルは、こちらのものを使っています。
https://raw.githubusercontent.com/logstash-plugins/logstash-patterns-core/v2.0.2/patterns/grok-patterns
https://github.com/logstash-plugins/logstash-patterns-core/blob/v2.0.2/patterns/grok-patterns
今回はApacheのアクセスログを対象にしたので、「COMBINEDAPACHELOG」を選びました。
COMBINEDAPACHELOGは、複数のパターンの組み合わせから成っていて、こんな感じになっています。
COMMONAPACHELOG %{IPORHOST:clientip} %{HTTPDUSER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest})" %{NUMBER:response} (?:%{NUMBER:bytes}|-) COMBINEDAPACHELOG %{COMMONAPACHELOG} %{QS:referrer} %{QS:agent}
ベースはCOMMONAPACHELOGですが、COMMONAPACHELOG自体も別のパターンからできていますね。
で、Apacheのアクセスログを対象にマッチをかけます、と。
Match match = grok.match("172.17.0.1 - - [16/Feb/2016:14:30:07 +0000] \"GET http://d.hatena.ne.jp/Kazuhira/20160214/1455460595 HTTP/1.1\" 200 52945 \"http://d.hatena.ne.jp/Kazuhira/\" \"Wget/1.15 (linux-gnu)\""); match.captures(); Map<String, Object> map = match.toMap(); assertThat(map) .contains(MapEntry.entry("clientip", "172.17.0.1"), MapEntry.entry("request", "http://d.hatena.ne.jp/Kazuhira/20160214/1455460595"), MapEntry.entry("agent", "Wget/1.15 (linux-gnu)"));
clientip、requestなどの名前から、キャプチャした値が取れていますね。
まとめ
embulk-parser-grokで使われているGrok for Javaを単体で、試してみました。
それなりに手軽に使えそうなのと、EmbulkやLogstashでもプラグインとして存在していることを知っていると、ログ解析の時などに便利そうだなーと思いました。
覚えておきましょう。
追記)
Grok Debuggerなるものがあることを教えていただきました。
https://grokdebug.herokuapp.com/
オンラインで、パターンに対して文字列をマッチさせて結果を確認できるようです。Grokを使う時は、知っていると助かりそうですね。