Logstashで二重に登録しないために

# 概要

elastic社のブログに「[Little Logstash Lessons: Handling Duplicates](https://www.elastic.co/blog/logstash-lessons-handling-duplicates)」という内容があがっています。
Google翻訳のお力を頂戴して、日本語で内容を確認したその結果です。

# 結論

elasticsearchにoutputするとき、IDが明示的に指定されていないと、elasticsearch側でIDが採番される。
これが重複して登録する元になっている。

なので、[fingerprint filter](https://www.elastic.co/guide/en/logstash/current/plugins-filters-fingerprint.html)を使って以下のようにアプローチすると良い。

ともにfingerprintを使うのは同じ。

- 同一の中身のものは、同一のものとして扱いたいとき
  - **同じ内容のものは同じID**になるようにし、その値をDocumentのIDとする
- １回のイベントで送信したものは再送されたとしても、同一のものとして扱いたいとき
  - **イベントごとにUUIDを生成**し、その値をDocumentのIDとする

```groovy 
input {
  stdin {}
}
filter {
  fingerprint {
    source => "message"
    target => "[@metadata][fingerprint]"
    method => "MURMUR3"
  }
}
output {
  elasticsearch {
    hosts => "example.com"
    document_id => "%{[@metadata][fingerprint]}"
  }
}
```
# 試した環境

| product | version |
|---------|---------|
| Logstash | 5.4.1 |

なお、logstashの出力結果にホスト名がありますが、記事用に書き換えたものです。

# 同一内容のものの重複を避ける

## stdoutによる確認

まずは、記事にあった内容でそのまま確認します。
[@metadata][fingerpring]に出力していると、**metadata => trueにしていないと**stdoutで確認できないため、便宜的にfingerprintという項目にfilter結果を入れるように変更します。

```groovy
input {
  stdin {}
}
filter {
  fingerprint {
    source => "message"
    target => "fingerprint"
    method => "MURMUR3"
  }
}
output {
  stdout {
     codec => rubydebug
  }
}
```

もし、targetを[@metadata][fingerprint]にしている場合なら、rubydebugにmetadata=>trueを足しましょう。

```groovy
//略
filter {
  fingerprint {
    source => "message"
    target => "[@metadata][fingerprint]"
    method => "MURMUR3"
  }
}
output {
  stdout {
     codec => rubydebug {
        metadata => true
     }
  }
}
```


aiueoと入力した場合の結果がこちら。fingerprintのところを見てください。
もし、fingerprintの項目をIDに指定していたとしたら、aiueoというmessageを持つものは、IDがすべて4063197173となり、
２回、３回、ｎ回送っても、IDが重複している限り１件のデータにしかならない、ということでしょう。

```
{
     "@timestamp" => 2017-06-06T06:44:48.590Z,
       "@version" => "1",
           "host" => "UCC-COFFEE-110YEN",
    "fingerprint" => 4063197173,
        "message" => "aiueo\r"
}
```

## 関数を変える

MURMUR3でなく、MD5がいいんや、という場合は、methodを変更します。

```
input {
  stdin {}
}
filter {
  fingerprint {
    source => "message"
    target => "fingerprint"
    method => "MD5"
    key => "hogehoge"
  }
}
output {
  stdout {
     codec => rubydebug
  }
}
```

同じようにaiueoと入れた結果がこちら。

```
{
     "@timestamp" => 2017-06-06T06:50:35.501Z,
       "@version" => "1",
           "host" => "UCC-COFFEE-110YEN",
    "fingerprint" => "03944d6aa32124dc928257becfc40299",
        "message" => "aiueo\r"
}
```

methodで、SHA1やMD5を使う場合は、keyオプションが必要です。

> If you are using any of the cryptographic hash functions algorithms (like SHA1, MD5), it is required to provide a key option. The key can be any arbitrary string which is used to compute the HMAC.

もし、keyを指定せずにlogstashを実行した場合は、次のエラーメッセージを含んだ文字列が出力されて終了します。

```
"Cannot register filter fingerprint plugin. The error reported is:
              Key value is empty. Please fill in an encryption key"
```

keyは設定するようにしましょう。


## fingerprint生成元の指定

今回は`source => "message"`としていたため、messageに入っている文字列から生成していました。

[source](https://www.elastic.co/guide/en/logstash/current/plugins-filters-fingerprint.html#plugins-filters-fingerprint-source)はarrayで指定できるため、複数の項目から値を生成したい場合も可能です。
複数の列が集まってPrimary Keyを作っているDBテーブルのようなものをイメージすればよいと思います。

たとえば、company_cd, code, parameterのようなもので１意にしたい、ということであれば

```
source => ["company_cd", "code", "parameter"]
```

のような書き方になるでしょう。

もし、任意の項目ではなく、すべての項目の文字列を１つの文字列のように連結したとして、そこからfingerprintを生成したい、ということであれば、`concatenate_sources => "true"`　を設定してやればよいでしょう。

```
input {
  stdin {}
}
filter {
  fingerprint {
    #source => "message"
    concatenate_sources => "true"
    target => "fingerprint"
    method => "MD5"
    key => "hogehoge"
  }
}
output {
  stdout {
     codec => rubydebug
  }
}
```

先と同じようにaiueoと入れた場合でも、結果が変わっていることがわかります。

```
{
     "@timestamp" => 2017-06-06T06:55:36.683Z,
       "@version" => "1",
           "host" => "UCC-COFFEE-110YEN",
    "fingerprint" => "3029457907e46196f8510cccfa1ff3ba",
        "message" => "aiueo\r"
}
```

# 不慮の重複を避ける

Logstashが何らかの影響で処理中に停止し、キューに入ったものが２回送られてしまうようなときを想定する。
少なくとも1回は送信されるけど、まれに2回送信されるかもね、というのはAWSのLambdaでも聞いたような気がします。

```
input {
  stdin {}
}
filter {
  fingerprint {
    source => "message"
    target => "generated_id"
    method => "UUID"
  }
}
output {
  stdout {
     codec => rubydebug
  }
}
```

このようにして、aiueoと2回送信してみます。1回目と2回目で同じ文字列を送っているのに、異なるgenerated_idとなっていることが確認できました。

generated_idをdocument_idとしておけば、キューが2回送信されたとしても重複しなくて済む、ということですね。

```
aiueo
{
      "@timestamp" => 2017-06-06T07:11:40.978Z,
        "@version" => "1",
            "host" => "UCC-COFFEE-110YEN",
         "message" => "aiueo\r",
    "generated_id" => "647943f7-1cb2-40f1-9e46-471a641f1651"
}
aiueo
{
      "@timestamp" => 2017-06-06T07:11:42.332Z,
        "@version" => "1",
            "host" => "UCC-COFFEE-110YEN",
         "message" => "aiueo\r",
    "generated_id" => "7d5e0691-b37c-4df1-82b5-3968b4bdbc29"
}
```

# 終わりに

> As you've seen in this post, the fingerprint filter can serve multiple purposes and is a plugin you should familiarize yourself in the Logstash ecosystem. Take a stab at using this filter and let us know what you think!

fingerprint filterはいろいろ使えますよ、と。
もっと起動が早くなればいうことなしなんだけどな。

# 参考文献

https://www.elastic.co/guide/en/logstash/current/plugins-filters-fingerprint.html