User Agentを解析するIngest Pluginを書いてみた

2016.07.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

User Agentを解析する公式プラグインがリリースされていました。
https://www.elastic.co/guide/en/elasticsearch/plugins/master/ingest-user-agent.html

はじめに

こんにちは、藤本です。

先日エントリしたElasticsearchのIngest Nodeを試してみたの中でProcessorのPluginがあることをご紹介しました。今回はUser Agentヘッダの文字列を解析するIngest Pluginを書いてみたので簡単に書き方をご紹介します。

概要

Elasticsearchの5系からIngest Nodeが追加され、Elasticsearch側で入力データを加工/変換できるようになります。それにより、データ送信元でデータ加工/変換を行っている環境では生のデータを送信するだけでよくなり、データ加工/変換用のリソースを抑えることができ、本来のアプリケーションだけにリソースを割り当てることができます。またデータ加工/変換用に別途サーバーを用意していた環境ではそのサーバーが必要なくなる可能性があります。

ただ先日のエントリで現在は20(Plugin含む)のProcessorが実装されていることをご紹介しましたが、Logstash/Fluentdをご利用の方は物足りなさを感じるでしょう。そこで今回はIngest Pluginのソースコードを読んでみると比較的簡単に実装できそうだったので実装方法をご紹介します。

Ingest Nodeの説明やIngest Pluginの書き方に関して、Elastic社のスライドで上がっていましたのでご参照ください。

今回作成するPlugin

Ingest PluginにIPアドレスからローケーション情報を解析するGeoIP Processorがあります。今回はhttpdのアクセスログ解析でGeoipとともによく利用される?であろうUser Agentを解析するIngest Pluginを作成します。

ソースコードはGithubにアップしました。

環境

  • 開発環境
    • OS : OS X Yosemite 10.10.5
    • Java : 1.8.0_72
    • Elasticsearch : 5.0.0-alpha4?(2016/06/28時点のMasterブランチ)
    • Gradle : 2.13

事前準備

ElasticsearchのソースコードをGithubからクローンしておきます。

ディレクトリ構成

クローンしたelasticsearchのpluginsディレクトリ配下に配置します。

elasticsearch  
├ setting.gradle // (1)
:
└ plugins  
 └ ingest-useragent // (以下、新規)
   ├ build.gradle // (2)
   ├ licenses // (3)
   │   ├ woothee-java-1.4.0.jar.sha1  
   │   ├ woothee-java-LICENSE.txt  
   │   └ woothee-java-NOTICE.txt  
   └ src  
     └ main  
         └ java  
          └ org  
            └ elasticsearch  
              └ ingest  
                └ useragent  
                  ├ IngestUserAgentPlugin.java  // (4)
                  └ UserAgentProcessor.java  // (5)

Gradle設定

Gradleを利用しない場合は、本章はスキップすることも可能です。Elasticsearch全体がGradleを利用していて、Plugin開発用の実装も用意されているのでGradleの利用を推奨します。

(1) setting.gradle

ElasticsearchのRootディレクトリにあるsetting.gradleに作成するPluginの名前を追加します。これにより、GradleのElasticsearch Plugin用のタスクを利用したり、GradleによるElasticsearch起動時にPluginを読み込むことが可能になります。

rootProject.name = 'elasticsearch'

List projects = [
  :
  'plugins:ingest-useragent', // (a)
  :
]
:
(a) Plugin追加

今回作成するPluginを追加します。pluginsディレクトリに作成するPluginのディレクトリ名に合わせてください。

(2) build.gradle

Plugin用のbuild.gradleを作成します。

esplugin { // (a)
    description 'Ingest processor that uses analyzed User Agent'
    classname 'org.elasticsearch.ingest.useragent.IngestUserAgentPlugin'
}

dependencies { // (b)
    compile('is.tagomor.woothee:woothee-java:1.4.0')
}

thirdPartyAudit.excludes = [
        'org.ho.yaml.Yaml', // (c)
]
(a) Plugin用のGradle追加

PluginはElasticsearch起動時にPluginディレクトリ下にあるplugin-descriptor.propertiesという定義ファイルを介して読み込まれます。espluginを定義しておくと、Gradleのタスクによってplugin-descriptor.propertiesを自動生成します。
classnameにPluginクラスを継承したクラスのパスを指定します。

(b) 依存ライブラリ

利用するライブラリを指定します。今回はUserAgentの解析にwootheeを利用します。

(c) 除外クラス

Pluginに利用しないクラスを除外クラスとして指定します。

Plugin初期化クラス

build.gradleespluginで指定したクラスを作成します。Elasticsearch起動時に読み込まれてElasticsearchのプラグインとして取り込まれます。

Pluginクラスを継承し、onModuleメソッドを実装します。

public class IngestUserAgentPlugin extends Plugin {
    public void onModule(NodeModule nodeModule) throws IOException {
        nodeModule.registerProcessor(UserAgentProcessor.TYPE,
                (registry) -> new UserAgentProcessor.Factory()); // (a)
    }
}
(a) Processor登録

実装するIngest PluginをElasticsearchで利用可能なProcessorとして登録します。

Processorクラス

処理を実装するクラスになります。

public final class UserAgentProcessor extends AbstractProcessor {

    public static final String TYPE = "useragent";
    private final String field;
    private final String targetField;

    UserAgentProcessor(String tag, String field, String targetField) {
        super(tag);
        this.field = field;
        this.targetField = targetField;
    }

    public void execute(IngestDocument ingestDocument) { // (c)
        String userAgent = ingestDocument.getFieldValue(field, String.class);  // (d)
        Map<String, String> u = Classifier.parse(userAgent); // (e)

        ingestDocument.setFieldValue(this.targetField, u); // (f)
    }

    @Override
    public String getType() {
        return TYPE;
    }

    public static final class Factory extends AbstractProcessorFactory<UserAgentProcessor> { // (a)
        public Factory() {}

        public UserAgentProcessor doCreate(String processorTag, Map<String, Object> config) throws Exception {
            String userAgentField = readStringProperty(TYPE, processorTag, config, "field"); // (b)
            String targetField = readStringProperty(TYPE, processorTag, config, "targetField", "useragent"); // (b)
            return new UserAgentProcessor(processorTag, userAgentField, targetField);
        }
    }
}
(a) インスタンス生成クラス

Factoryクラスを実装します。FactoryクラスではdoCreateメソッドを実装します。

(b) オプション設定

Processorにて指定可能なフィールドを定義します。今回はUserAgentの文字列が入力されるfield、解析したUserAgentのフィールドとなるtargetFieldを定義します。デフォルト値の設定も可能です。

(c) 処理

executeメソッドに処理を記述します。

(d) User Agent取得

ドキュメントからfieldで指定したフィールドの値を取得します。

(e) User Agent解析

wootheeライブラリを使用して、User Agentの文字列から、端末・OS名・OSバージョン・ベンダ・ブラウザ名・ブラウザバージョンを解析します。

(f) ドキュメントへのフィールド追加

ドキュメントに対して解析したUser Agentを追加します。Map型で追加するとElasticsearchではObject Typeのように扱われます。

動作確認

Simulate APIを利用して、作成したIngest Pluginが正常に動作するか確認してみましょう。

GradleでElasticsearchを起動します。(今回テストは未実装なため、除外します)

# cd $ELASTICSEARCH_ROOT/plugins/ingest-useragent
# gradle run -x test -x integTest
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:writeVersionProperties UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE

:

:plugins:ingest-useragent:compileJava
:plugins:ingest-useragent:processResources UP-TO-DATE
:plugins:ingest-useragent:classes
:plugins:ingest-useragent:jar
:plugins:ingest-useragent:copyPluginPropertiesTemplate
:plugins:ingest-useragent:pluginProperties UP-TO-DATE
:plugins:ingest-useragent:bundlePlugin
:plugins:ingest-useragent:run#prepareCluster.cleanShared
:distribution:integ-test-zip:buildZip UP-TO-DATE
:plugins:ingest-useragent:run#clean
:plugins:ingest-useragent:run#checkPrevious SKIPPED
:plugins:ingest-useragent:run#stopPrevious SKIPPED
:plugins:ingest-useragent:run#extract
:plugins:ingest-useragent:run#configure
:plugins:ingest-useragent:run#copyPlugins
:plugins:ingest-useragent:run#installIngestUseragentPlugin
:plugins:ingest-useragent:run#start
[elasticsearch] [2016-07-15 16:41:27,376][INFO ][node                     ] [Mother Night] version[5.0.0-alpha4-SNAPSHOT], pid[73439], build[18d45b2/2016-06-29T06:54:05.832Z], OS[Mac OS X/10.10.5/x86_64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_72/25.72-b15]
[elasticsearch] [2016-07-15 16:41:27,376][INFO ][node                     ] [Mother Night] initializing ...
[elasticsearch] [2016-07-15 16:41:27,507][INFO ][plugins                  ] [Mother Night] modules [], plugins [ingest-useragent]
:
[elasticsearch] [2016-07-15 16:41:32,536][INFO ][node                     ] [Mother Night] started

Simulate APIでuseragentProcessorを利用して、ドキュメントを変換します。

### Simulate API
# curl -XPOST "http://localhost:9200/_ingest/pipeline/_simulate?pretty" -d'
{
  "pipeline" : {
    "processors" : [
      {
        "useragent" : {
          "field" : "message"
        }
      }
    ]
  },
  "docs" : [
    {
      "_source" : {
        "message" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36"
      }
    }
  ]
}'

### Output
{
  "docs" : [
    {
      "doc" : {
        "_id" : "_id",
        "_index" : "_index",
        "_type" : "_type",
        "_source" : {
          "useragent" : {
            "name" : "Chrome",
            "category" : "pc",
            "os" : "Mac OSX",
            "version" : "32.0.1700.77",
            "vendor" : "Google",
            "os_version" : "10.9.1"
          },
          "message" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36"
        },
        "_ingest" : {
          "timestamp" : "2016-07-15T07:39:32.943+0000"
        }
      }
    }
  ]
}

messageフィールドのUser Agent文字列を元に、 useragentフィールドにnamecategoryosversionos_versionというフィールド名で各種値が追加されました。

Elasticsearchへのデプロイ

次にLinuxサーバ上で動作するElasticsearchにデプロイしてみましょう。

Gradleのビルドによりデプロイ用のzipファイルを生成します。

# cd $ELASTICSEARCH_ROOT/plugins/ingest-useragent
# gradle build -x test -x integTest
:

BUILD SUCCESSFUL

Total time: 32.725 secs

zipファイルはbuild/distributions配下に生成されています。

Linuxサーバに生成されたzipファイルを転送します。

# scp build/distributions/ingest-useragent-5.0.0-alpha4-SNAPSHOT.zip username@hostname:/tmp/

Linuxサーバにログインし、転送したzipファイルをPluginとしてインストールします。

(Linux)# /usr/share/elasticsearch/bin/elasticsearch-plugin install file:///tmp/ingest-useragent-5.0.0-alpha4-SNAPSHOT.zip
-> Downloading file:///tmp/ingest-useragent-5.0.0-alpha4-SNAPSHOT.zip
-> Installed ingest-useragent

Pluginsディレクトリ配下にコンポーネントが配置されます。

(Linux)# ls -l /usr/share/elasticsearch/plugins/ingest-useragent/
total 64
-rw-r--r--. 1 root root  8164 Jul 15 08:02 ingest-useragent-5.0.0-alpha4-SNAPSHOT.jar
-rw-r--r--. 1 root root  1318 Jul 15 08:02 plugin-descriptor.properties
-rw-r--r--. 1 root root 49900 Jul 15 08:02 woothee-java-1.4.0.jar

実装したPluginのjarファイル、依存ライブラリ、Pluginの定義ファイルとなるplugin-descriptor.propertiesが展開されています。

Elasticsearchを再起動し、Pluginを読み込ませます。

(Linux)# systemctl restart elasticsearch
(Linux)# curl -XPOST "http://localhost:9200/_ingest/pipeline/_simulate?pretty" -d'
{
  "pipeline" : {
    "processors" : [
      {
        "useragent" : {
          "field" : "message"
        }
      }
    ]
  },
  "docs" : [
    {
      "_source" : {
        "message" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36"
      }
    }
  ]
}'

### Output
{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_type",
        "_id" : "_id",
        "_source" : {
          "useragent" : {
            "name" : "Chrome",
            "category" : "pc",
            "os" : "Mac OSX",
            "version" : "32.0.1700.77",
            "vendor" : "Google",
            "os_version" : "10.9.1"
          },
          "message" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36"
        },
        "_ingest" : {
          "timestamp" : "2016-07-15T08:03:30.042+0000"
        }
      }
    }
  ]
}

各種情報が展開されました。

まとめ

いかがでしたでしょうか?
Javaをあまり書き慣れていない私でも簡単に実装することができました。LogstashやFluentdにないデータ加工処理も独自実装で追加できそうです。