CLOVER🍀

That was when it all began.

テキスト解析にGrok for Java

こちらのエントリを見て、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の紹介

このembulk-parser-grokを見ていると、grokというものが単体で使えそうだったので、ちょっとメモとして。

Grok for java

なお、この流れで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

とりあえず、このあたりを見ながら写経的に。

How to use

Create a Grok Pattern

書いたコードは、こちら。

    @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だけっぽい雰囲気もありますが。

https://github.com/thekrakken/java-grok/blob/grok-0.1.1/src/main/java/oi/thekraken/grok/api/Match.java#L164

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");

使われているのは、これみたいですね。

named-regexp

文字列がパターンにマッチしない場合

先にも近いことを書きましたが、パターンにマッチしない場合は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を繰り返し呼び出している感じですが。

https://github.com/thekrakken/java-grok/blob/grok-0.1.1/src/main/java/oi/thekraken/grok/api/Grok.java#L254

では、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を使う時は、知っていると助かりそうですね。