Splunk のカスタムサーチコマンド作成 EventingCommand 編

Splunk の EventingCommand 型のカスタムサーチコマンドを作成します。下の記事では、GeneratingCommand (データ生成型コマンド)を作成しました。

* [Splunk カスタムサーチコマンド (custom search command) 作成 ― はじめの一歩 ― - Qiita](https://qiita.com/msi/items/02d029d655e1f9285806)
* [Splunk カスタムサーチコマンド (custom search command) 作成 その2 - Qiita](https://qiita.com/msi/items/ca5d3c553bd49d8665ac)

今回は、EventingCommand を作成します。

"EventingCommand" とは？ という方は前の記事の「[カスタムサーチコマンドの4つの種類](https://qiita.com/msi/items/02d029d655e1f9285806#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%B5%E3%83%BC%E3%83%81%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AE4%E3%81%A4%E3%81%AE%E7%A8%AE%E9%A1%9E)」をご覧ください。
開発環境についても SDK 等の説明は省きます。

# ゴール
前回の出力 "`hello, world`" を受け取り、すべての文字を大文字に変換するコマンド `toupper` を作ってみます。

```text:SPL&nbsp;例
| generatehello2 | toupper greeting     ← "greeting" はフィールド名
```

eval の upper() に相当するものをフィールド指定で変換するものになります。

# Version 1 protocol と Version 2 protocol の違い

始める前に、サーチコマンドのプロトコル Version 1 と Version 2 について書いておきます。

Python のカスタムサーチコマンド作成の紹介記事は色々とありますが、主に以下の手法を使っています。

1. 作成したスクリプト(コマンド)を `run` コマンドを用いて実行する。
2. `run` を使用せず、内部で `splunk.InterSplunk` モジュールを利用する。

これらは、インプットとアウトプットの処理をどうするかという違いはありますが、基本的な違いはありません。
1つ目は splunk から呼び出し可能であればどのような形のものでも実行できます。
2つ目は `splunk.InterSplunk` を使って入力と出力を行います。

これらの利用は、protocol Version 1 あるいは、legacy protocol と呼ばれる従来の方法で呼び出されます。

```text:commands.conf.spec
chunked = [true|false]
* If set to "true", this command supports the new "chunked" custom
  search command protocol.
* If set to "true", the only other commands.conf settings supported are
  'is_risky', 'maxwait', 'maxchunksize', 'filename', 'command.arg.<N>', and
  'run_in_preview'.
* If set to "false", this command uses the legacy custom search command
  protocol supported by Intersplunk.py.
* Default: false
```
"[commands.conf - Splunk Documentation](https://docs.splunk.com/Documentation/Splunk/latest/Admin/Commandsconf)" より引用

Version 1 を使用する場合には、commands.conf の設定に chuncked = false を設定するか、書かずにデフォルトに任せて false のままにしておきます。

一方、Version 2 の新しいカスタムサーチコマンドを利用する場合には、splunk.InterSplunk を利用せずに `EventingCommand` `TransformingComannd` `GeneratingCommand` `StreamingCommand` のクラスを利用します。

Version 1 protocol と Version 2 protocol には次のような違いがあります。

<blockquote>
<h2><i class="glyphicon glyphicon-chevron-down"></i><span class="mw-headline" id="About_the_protocols">About the protocols</span></h2>
<h3><span class="mw-headline" id="Version_2_protocol">Version 2 protocol</span></h3>
<p>There are significant advantages to using the Version 2 of the Custom Search Command protocol.
</p>
<ul><li><div class="li_content">With the Version 2 protocol, external programs process through an entire set of Splunk search results in one invocation. The external program is invoked once for the entire search, greatly reducing runtime overhead.</div></li>
<li><div class="li_content">The Version 2 protocol requires fewer configuration attributes than the Version 1 protocol.</div></li>
<li><div class="li_content">Supports non-Python custom search commands, for example C++, Go, Java and JavaScript (Node.js).</div></li>
<li><div class="li_content">Support for platform-specific executable files and binaries. You can write custom search commands in compiled languages, such as C++, on multiple platforms.</div></li></ul>
<h3><span class="mw-headline" id="Version_1_protocol">Version 1 protocol</span></h3>
<p>The Version 1  of the Custom Search Command protocol processes events in groups, using 50,000 events for each group. The external program is invoked and terminated multiple times for large result sets. 
</p>
</blockquote>
"[About writing custom search commands - Splunk Documentation](https://docs.splunk.com/Documentation/Splunk/latest/Search/Aboutcustomsearchcommands)" より引用

v2 になると、Python 以外にもサポートされる言語が増えますが、一番大きな違いはイベント 5万件ごとにプログラムが呼び出されないというところでしょう。逆に言えば、**v1 ではイベント 5万件ごとにカスタムサーチコマンドが起動されます**ので、パフォーマンスに違いが出ます。
5万件を超えないイベントを扱うのであればどちらでも差はありませんが、Splunk ではイベントがすぐに 5万件を超えますので、これから作るのであれば、`splunk.InterSPlunk` を使わずに、v2 を使うとよいでしょう。

この記事では、Version 2 protocol を用いたカスタムサーチコマンドを作成します。

# 準備

1. [前回](https://qiita.com/msi/items/ca5d3c553bd49d8665ac)の環境がある場合にはそのまま継続して使用します。
2. この記事から始める場合には、[GitHub](https://github.com/s-yamano/Splunk-CustomSearchCommand-FirstStep) から step1-2 をダウンロードし、 `HelloWorld-work` ディレクトリを `$SPLUNK_HOME/etc/apps/HelloWorld/` にコピーして Splunk を再起動します。
3. `$SPLUNK_HOME/etc/apps/HelloWorld/lib/` を作成し、その下に [SDK](https://github.com/splunk/splunk-sdk-python/releases/latest) の `splunklib` をコピーします。  
    (`pip3 install -t  lib/ splunk-sdk` でも SDK ライブラリがインストール可能です。)

ディレクトリとファイルの構造は次のようになっています。

```text:${SPLUNK_HOME}/etc/apps/
HelloWorld/
├─ bin
│   ├─ HelloWorld.py
│   ├─ HelloWorld2.py
│   ├─ filter.py
│   ├─ generate.py
│   ├─ report.py
│   └─ stream.py
├─ default
│   ├─ app.conf
│   ├─ commands.conf
│   └─ data
│       └─ ui
│           └─ nav
│               └─ default.xml
├─ lib
│   ├─ splunk_sdk-1.6.12.dist-info
│   │   (pip でインストールした場合にはこのディレクトリが存在します。)
│   └─ splunklib
│       ├─ __init__.py
│       ... (省略)
└─ metadata
    └─ default.meta
```

# 開発

## ファイルの作成

`bin/` 下にある `filter.py` をコピーして `ToUpper.py` を作成します。

```shell_session
cd bin/
cp filter.py ToUpper.py
```

元にした `filter.py` は、EventingCommand のテンプレートなのですが、メンテナンスされていないようで、そのままでは使用できません。 (SDK v1.6.12)

```python:ToUpper.py
#!/usr/bin/env python

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from splunklib.searchcommands import \
    dispatch, StreamingCommand, Configuration, Option, validators


@Configuration()
class %(command.title())Command(EventingCommand):
    """ %(synopsis)

    ##Syntax

    %(syntax)

    ##Description

    %(description)

    """
    def transform(self, events):
       # Put your event transformation code here
       pass

dispatch(%(command.title())Command, sys.argv, sys.stdin, sys.stdout, __name__)
```

import で `StreamingCommand` になっている部分を `EventingCommand` に修正します。

```diff:diff
--- filter.py
+++ ToUpper.py
@@ -5,8 +5,8 @@

 sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
 from splunklib.searchcommands import \
-    dispatch, StreamingCommand, Configuration, Option, validators
+    dispatch, EventingCommand, Configuration, Option, validators
```

```python:ToUpper.py&nbsp;修正後
#!/usr/bin/env python

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from splunklib.searchcommands import \
    dispatch, EventingCommand, Configuration, Option, validators


@Configuration()
class %(command.title())Command(EventingCommand):
    """ %(synopsis)

    ##Syntax

    %(syntax)

    ##Description

    %(description)

    """
    def transform(self, events):
       # Put your event transformation code here
       pass

dispatch(%(command.title())Command, sys.argv, sys.stdin, sys.stdout, __name__)
```

## 編集

1. `%(command.title())` の部分をすべて `ToUpper` に修正します。
2. `transform()` が必須の本体になりますので、この部分に処理を追加します。 (解説は後述)

```diff:diff
--- ToUpper.py.orig
+++ ToUpper.py
@@ -9,7 +9,7 @@


 @Configuration()
-class %(command.title())Command(EventingCommand):
+class ToUpperCommand(EventingCommand):
     """ %(synopsis)

     ##Syntax
@@ -23,6 +23,11 @@
     """
     def transform(self, events):
        # Put your event transformation code here
-       pass
+        for event in events:
+            for field in self.fieldnames:
+                if field in event:
+                    event[field] = event[field].upper()
+            yield event
+        return

-dispatch(%(command.title())Command, sys.argv, sys.stdin, sys.stdout, __name__)
+dispatch(ToUpperCommand, sys.argv, sys.stdin, sys.stdout, __name__)
```

```python:ToUpper.py&nbsp;更新後
#!/usr/bin/env python

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from splunklib.searchcommands import \
    dispatch, EventingCommand, Configuration, Option, validators


@Configuration()
class ToUpperCommand(EventingCommand):
    """ %(synopsis)

    ##Syntax

    %(syntax)

    ##Description

    %(description)

    """
    def transform(self, events):
       # Put your event transformation code here
        for event in events:
            for field in self.fieldnames:
                if field in event:
                    event[field] = event[field].upper()
            yield event
        return

dispatch(ToUpperCommand, sys.argv, sys.stdin, sys.stdout, __name__)
```

* `transform()` は `EventingCommand` の必須メソッドで、`dispatcher` によって呼び出される。
* `transform()` の第2引数 (ここでは、`events`) は各イベントのリスト。
* 引数に指定したフィールド (`option=xxxx` という形をしていない引数) は、`self.fieldnames` にリストとして渡される。
* フィールドの参照および代入は、各イベントの辞書リスト (上では `event[field]`) 形式で行える。

というのが、EventingCommand の要素です。

上の例では、各イベントに対し (`for event in events:`) 指定された各フィールド (`for field in self.fieldnames:`) ごとに、大文字に変換して、代入し直して (`event[field] = event[field].upper()`) います。

## 動作確認

### コマンドラインによる動作確認

Splunk と切り離したコマンドライン上で動作確認をします。

ローカルに実行するためには、`__EXECUTE__` を指定して実行します。

```shell_session
python3 ToUpper.py __EXECUTE__ </dev/nul
```

何もエラーがなければ成功です。

入力に `/dev/null` を指定しているので、出力も無いのですが、実は、入力を行っても何も表示されません。

前回作成した `HelloWorld2.py` を使って、試してみます。

```shell_session
$ python3 HelloWorld2.py __EXECUTE__ count=3 < /dev/null  # HelloWorld2.py 単体での動作確認

_time,__mv__time,greeting,__mv_greeting,_raw,__mv__raw
1589160289.7283704,,"hello, world",,"hello, world",
1589160289.7284214,,"hello, world",,"hello, world",
1589160289.7284305,,"hello, world",,"hello, world",
$ python3 HelloWorld2.py __EXECUTE__ count=3 < /dev/null | python3 ToUpper.py __EXECUTE__ greeting
$                     ← 何も表示されずにコマンドプロンプトに戻ってくる。
```

注: コマンドラインで表示させる方法を見つけられませんでした。ご存知の方がいらっしゃいましたら、ご教授いただけると幸いです。

ひとまず、エラーの有無は確認できると思います。

### サーチによる動作確認

Splunk のサーチコマンドとして動作確認を行います。

#### 設定

提供されたものを修正する場合には、`local/` ディレクトリ下にファイルを作成して設定するのですが、今回は「提供者」という立場で、`default/` 下のファイルを変更します。

##### commands.conf
`default/commands.conf` を編集します。

```diff
--- commands.conf.orig
+++ commands.conf
@@ -10,3 +10,8 @@
 filename = HelloWorld2.py
 chunked = true
 python.version = python3
+
+[toupper]
+filename = ToUpper.py
+chunked = true
+python.version = python3
```

* Protocol Version 2 を使用しますので、`chunked = true` を設定します。
* Python2 は、2020年1月1日で EOL を迎えたので、Python3 を使用します。
* commands.conf の詳細は「[commands.conf - Splunk Documentation](https://docs.splunk.com/Documentation/Splunk/latest/Admin/Commandsconf)」を参照してください。


```ini:default/commands.conf
# [commands.conf]($SPLUNK_HOME/etc/system/README/commands.conf.spec)
# Configuration for Search Commands Protocol version 2

[generatehello]
filename = HelloWorld.py
chunked = true
python.version = python3

[generatehello2]
filename = HelloWorld2.py
chunked = true
python.version = python3

[toupper]
filename = ToUpper.py
chunked = true
python.version = python3
```

##### searchbnf.conf および default.meta

今回は、Tips として、`searchbnf.conf` と `default.meta` を設定します。

`searchbnf.conf` は新規に作成します。

```ini:searchbnf.conf
[toupper-command]
syntax = toupper <field-list>
description = toupper command to transfer into upper cases by the specify fields.\
        This is a long description.
shortdesc = make fields contents to upper cases.
usage = public
```

参照: [searchbnf.conf - Splunk Documentation](https://docs.splunk.com/Documentation/Splunk/latest/Admin/Searchbnfconf)

* `[toupper-command]` の記述のうち、_toupper_ 部分は `commands.conf` で設定したサーチコマンド名、`-command` 部分は固定です[^1]。
* この設定を行うと、サーチ窓に入力したときにヘルプを表示できます。 (SPL エディタで「完全」か「コンパクト」を設定している時)
* `usage = public` を設定し忘れると、ヘルプは表示されません。

[^1]: 余談ですが、UNIX の roff の慣例では、可変部をイタリックで記載します。MarkDown 記法や HTML の弊害は、\`quote\` の中に可変部を示す _イタリック_ を書けないことでしょうか。どこが可変文字でどこが固定文字なのか区別がつかないので、毎回説明しなければなりません。逆に、説明がないとドキュメントだけではわかりづらいことがあります。

`metadata/default.meta` へ `searchbnf` について追加設定します。

```diff
--- default.meta.orig
+++ default.meta
@@ -1,2 +1,5 @@
 []
 access = read: [ * ], write : [ admin ]
+
+[searchbnf]
+export = system
```

```ini:metadata/default.meta
[]
access = read: [ * ], write : [ admin ]

[searchbnf]
export = system
```

#### 再起動

`splunkd` を再起動します[^2]。

[^2]: debug/refresh で有効になるかどうかは未確認。

```shell_session
sudo ${SPLUNK_HOME}/bin/splunk restart
```

または、systemctl で設定している場合には

```shell_session
sudo systemctl restart Splunkd
```

#### 動作確認
サーチ窓で動作を確認します。

##### 1. generatehello2 の確認。

```text:SPL文
| generatehello2 count=3
| table greeting
```

```text:実行結果
greeting
hello, world
hello, world
hello, world
```

##### 2. toupper の確認。

```text:SPL文
| generatehello2 count=3
| table greeting
| toupper greeting
```

```text:実行結果
greeting
HELLO, WORLD
HELLO, WORLD
HELLO, WORLD
```

うまく小文字が大文字に変換されました。

# まとめ
* 大文字に変換する toupper コマンドを作成しました。
* Protocol Version 2 の利用を推奨します。 (splunk.InterSplunk は使用しない)
* イベントを変換するコマンドは `EventingCommand` クラスを使用します。
* searchbnf.conf を設定することで、ヘルプを表示できるようになります。
* Python2 は 2020年1月1日で EOL を迎えたので、Python3 で動作することを確認しました。
* ソースコードは "[Splunk-CustomSearchCommand-FirstStep: Splunk カスタムサーチコマンド (custom search command) 作成 ― はじめの一歩 ― - Qiita のサンプル](https://github.com/s-yamano/Splunk-CustomSearchCommand-FirstStep)" にて公開しています。