重複するイベントを削除したい

# ことの背景
とある Add-on を使用しデータを取得していたところ、イベントデータの重複に気づきました。この Add-on は 10分毎に REST API からデータを取得するものなのですが、どうやらその 10分の境界付近のデータを二重に取得してしまっている模様です。

Splunk では `delete` コマンドでイベントの論理削除を行うことはできますが、さすがに 10分毎に確認して手動削除というわけにはいかないので、**自動的に重複しているイベントを削除したい**というのが今回の背景です。

# 解決方法
## 急いでいる人向け
長々読みたくない人向けに結論というか、汎用的に利用できそうなサーチ例を貼ります。

```
index="xxx" sourcetype="yyy"
| eval event_id=_cd
| search
    [search index="xxx" sourcetype="yyy"
    | eval hash=sha256(_raw)
    | streamstats count by hash
    | where count > 1
    | eval event_id=_cd
    | return 100 event_id] 
| delete
```

これを見て納得・理解できる方はご利用ください。
なぜこれでうまくいくのか、よくわからない方は続きを読んでください。

## 前提
上述しましたが、Splunk では一度取り込んでしまったデータは、インデックスごと消す以外ディスク上から消す術はありません。代わりに `delete` コマンドという論理的にデータを消す(サーチから見えなくする)が用意されています。

ただし、このコマンドはデフォルトではどのユーザ(ロール)から利用できないようになっています。間違って、簡単にデータを消してしまっては困るので、当たり前といえば当たり前です(論理的に削除と言っても、ディスク上にデータが残っているからといって、削除を取り消す方法はありません。少なくとも私は知りません)。

`delete` コマンドを利用できるようにするためには、`can_delete` という権限をユーザに付与しておく必要があります。この手順について、ここでは割愛します。

## 重複を探す
### まず思いつきそうな方法
まず、何をもってイベントが重複していると言えるのかというと、`_raw` が全く同じであるということしかなさそうです。ログ種別によっては、各イベントにユニークな ID が付いているなどといったラッキーなケースもあるとは思いますが、ここではそういったことはないという前提で考えます。

`_raw` を比較するしかない以上、重複を見つけるに

```
index="xxx" sourcetype="yyy"
| stats count by _raw
| where count > 1
```
とかやりますか？ 一般的な感覚はわかりませんが、個人的には `by _raw` というのが非常に気持ち悪いです。とはいえ、一応重複しているイベントは見つけられました。

で、次にこれを消そう、となるわけですが、大きく 2 つの問題があることに気づきます。それは、

1. `stats` コマンドで集計してしまっているので、そのまま `delete`コマンドに渡せない
2. 重複が必ずしも 2 イベントとは限らない

2つ目はそれほど大きな大きな問題ではないかもしれませんが、1つ目は大問題です。せっかく重複を見つけたのに、消せないので。

次に思いつくのが、サブサーチです。上記のサーチをサブサーチとして、結果をメインのサーチに返すというものです。具体的には、次のような感じのサーチになります。

```
index="xxx" sourcetype="yyy"
    [search index="xxx" host="yyy"
    | stats count by _raw 
    | return 100 _raw]
```
`return` コマンドは返却する結果の最大値を決める必要があるので、とりあえず 100 としています。ちなみに、サブサーチ部分だけ実行していただくとわかるのですが、サブサーチはメインのサーチに

```
(_raw=...) OR (_raw=...) OR ...
```

のような検索条件を返しています。`_raw` を条件にするのもかなり気持ち悪いですね。

といったところあたりまで考えて、私はこの方法は筋が悪そうなので諦めることにしました。
(実は筋が悪いだけではなく、他にも問題があることが後からわかりましたが、ここでは省略します)

### 良さそうな方法

まず、`_raw` をそのまま扱うのは気持ち悪い(具体的な根拠があるわけではありません)ので、イベントを区別するためにハッシュ値を `_raw` に対して計算します。

```
| eval hash=sha256(_raw)
```
これで、`_raw` が同じ内容であれば同じハッシュ値が得られるはずです。

次に、上述の問題点でもあった `stats` で集計してしまうと、結果を `delete` コマンドに渡せない点について考えます。イベントはそのまま残したい、でもハッシュ値毎の集計がしたいということで、`eventstats` が良さそうです。

つまり、

```
| eval hash=sha256(hash)
| eventstats count by hash
| table hash count
```
とします。こうすると、同じハッシュ値をもつイベント数をカウントして、その値を各イベントに新しいフィールド(count)を追加してくれます。イメージ的には、こんな感じです。

|hash|...|count|
|:---|:---:|---:|
|hash_a|...|3|
|hash_b|...|1|
|hash_a|...|3|
|hash_c|...|1|
|hash_a|...|3|

hash_a は 3 回現れるので、hash_a を持つイベントにはそれぞれ count に 3 が入ります。同様に hash_b, hash_c は 1 回ずつしか現れないので、count は 1 です。

この結果から count が 1 より大きい2以上)イベントが重複しており、削除対象とすればいいと判断できます。

これで全て解決と言いたいところですが、次の課題があります。それは、`count > 1` に該当するイベントを全て削除してはダメだということです。つまり、

```
| eval hash=sha256(hash)
| eventstats count by hash
| where count > 1
| delete
```
としてしまうと、重複していたイベントが全て消えてしまい、残ってほしい最後の 1 イベントもなくなってしまいます。

それではどうしたらいいか。重複しているイベントが何個重複しているかはわかっているわけなので(count)、それらに通し番号を振ってみればいいのでは、と考えました。ハッシュ値毎に通し番号を振るには `streamstats` が良さそうです。つまり、次のようなサーチになります。

```
| eval hash=sha256(hash)
| eventstats ev_count by hash
| streamstats st_count by hash
| table hash ev_count st_count
```

こうすると、結果は、

|hash|...|ev_count|st_count|
|:---|:---:|---:|---:|
|hash_a|...|3|1|
|hash_b|...|1|1|
|hash_a|...|3|2|
|hash_c|...|1|1|
|hash_a|...|3|3|

のような形になっているはずです。
各ハッシュ値をもったイベントを 1 つだけ残して他のイベントを `delete` コマンドに渡すには ev_count と st_count を比較してフィルタしてあげればいいです。

```
| eval hash=sha256(hash)
| eventstats count as ev_count by hash
| streamstats count as st_count by hash
| where ev_count > st_count
```

st_count の最大値は ev_count に一致しますから、`where` 文には引っかかりません。つまり 1 イベントだけを残すことができます。

### もっと良さそうな方法

ここまでやってきて、ふと気づきました 「`eventstats` いらなくね？」。

|hash|...|ev_count|st_count|
|:---|:---:|---:|---:|
|hash_a|...|3|1|
|hash_b|...|1|1|
|hash_a|...|3|2|
|hash_c|...|1|1|
|hash_a|...|3|3|

先程の表を見返します。重複があるイベントは st_count が 2 以上のものがあるわけなので、ev_count とか関係なく `st_count > 1` で十分じゃんと。

つまり、こんなサーチで良いのではと。

```
| eval hash=sha256(hash)
| streamstats count by hash
| where count > 1
```
思いのほかシンプルなサーチになってしまいました。

## 削除~~する~~できなかった

### 誤算

ようやく、これで `delete` コマンドへ渡すことができそうです。
先程のサーチ結果を `delete` コマンドに流し込みます。

```
| eval hash=sha256(hash)
| streamstats count by hash
| where count > 1
| delete
```
これで重複したイベントが華麗に消えてくれる、と思ったのですが、、、
![Error in 'delete' command: This command cannot be invoked after the command 'simpleresultcombiner', which is not distributable streaming.
](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1931706/ce2c92ab-dd3c-7ce2-6217-7eef1efdbc7d.png)
知りませんでした。`delete` コマンドは分散ストリーミングコマンドの後にしかもってこれないんですね…
分散ストリーミングコマンドというのは、シンプルにいうとインデクサ上で実行可能なコマンドだと捉えていただくと大きく間違っていないと思います。イベントを集計しなくてもできる、`eval` 等の個々のイベントに対して操作するコマンドです。正確なところは[こちら](https://docs.splunk.com/Documentation/Splunk/latest/Search/Typesofcommands#Distributable_streaming)をご確認ください。

対象となるイベント全部を調べて同じ内容のイベントを探すわけですから、分散ストリーミングコマンドだけで書くのは無理と断念。

### 妥協案
上記のサーチで実現できれば非常にシンプルで良かったのですが、致し方ないのでサブサーチを使う方針に変更。次のようなサーチを書きました。

```
index="xxx" sourcetype="yyy"
| eval event_id=_cd
| search
    [search index="xxx" sourcetype="yyy"
    | eval hash=sha256(_raw)
    | streamstats count by hash
    | where count > 1
    | eval event_id=_cd
    | return 100 event_id] 
| delete
```
基本的には、サブサーチで重複しているイベントを見つけて、それをメインのサーチで消すということをしているだけなのですが、重複イベントのハッシュ値をメインサーチに返すと、メインサーチで重複イベントを全て消してしまうことになりうれしくありません。

ということで、ここでは同一ハッシュでもイベントを区別するために、イベントを一意に区別する隠しフィールド "_cd" を使うことにしました。
(補足: [マニュアル](https://docs.splunk.com/Documentation/Splunk/8.2.1/Knowledge/Usedefaultfields)によると隠しフィールドではなく内部フィールドと呼ぶのが正しいようです。)

これでようやく完成です。初めはそんなに難しくないだろうと思っていたのですが、思いのほか苦労することとなりました。

繰り返しになりますが、`delete` コマンドで消してしまったイベントは二度と元には戻りませんから、ご利用は慎重にお願いします。このサーチを saved search にして定期実行すれば自動的に重複排除することもできそうですが、十分検証してからやったほうが良さそうですね。