CLOVER🍀

That was when it all began.

Fluent Bitでレコードを変更する(record_modifier、modify、lua)

これは、なにをしたくて書いたもの?

Fluent Bitの機能で、レコードを編集するものを試してみようかなと思いまして。

具体的には、以下の3つのFilterプラグインが該当します。

Modify - Fluent Bit: Official Manual

Record Modifier - Fluent Bit: Official Manual

Lua - Fluent Bit: Official Manual

Parserプラグインもある意味ではこの目的には近い気はしますが、また別の機会に。

Parser - Fluent Bit: Official Manual

それぞれ、簡単な説明を入れつつ試していってみましょう。

環境

今回の環境は、こちらです。

Fluent Bit。

$ /opt/td-agent-bit/bin/td-agent-bit --version
Fluent Bit v1.5.3

Ubutu Linux 20.04 LTSに、aptでインストールしています。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

まずは、デフォルトの設定ファイルを載せておきます。

$ grep -v '^ *#' /etc/td-agent-bit/td-agent-bit.conf 
[SERVICE]
    flush        5

    daemon       Off

    log_level    info

    parsers_file parsers.conf

    plugins_file plugins.conf

    http_server  Off
    http_listen  0.0.0.0
    http_port    2020

    storage.metrics on





[INPUT]
    name cpu
    tag  cpu.local

    interval_sec 1

[OUTPUT]
    name  stdout
    match *

ここから、TCP をInputにして、Outputを標準出力に設定します。

[INPUT]
    Name   tcp
    Listen 0.0.0.0
    Port   5170
    Format json


## FILTER定義

[OUTPUT]
    Name  stdout
    Match *
    Format json_lines

あとは、Filterプラグインの定義を変えていって、試していきましょう。

Record Modifier

まずは、Record Modifier Filterプラグインから。

Record Modifier - Fluent Bit: Official Manual

これは、フィールドの追加および削除ができるプラグインになります。とてもシンプルです。

Recordで指定のキーと値を追加、Remove_keyで指定のキーを削除することができます。

[FILTER]
    Name  record_modifier
    Match *
    Record hostname ${HOSTNAME}
    Record filter_type record_modifiler
    Record append_message Hello Fluent Bit!!
    Remove_key dummy_field

「${}」で環境変数を参照することもできます。

3つのキーを(hostname、filter_type、append_message)を追加し、ひとつのキー(dummy_field)を削除してみます。
hostnameは、環境変数を使用します。

ちなみに、次のModify Filterプラグインと違って、Recordに指定する値には空白を含めることができるみたいです。
※それを見るためのappend_messageです

確認してみましょう。

$ echo '{"message": "Hello World!!", "day": "2020-08-16", "dummy_field": "dummy"}' | nc localhost 5170

結果。

Aug 16 12:00:26 myhost td-agent-bit[1177]: {"date":1597579225.752374,"message":"Hello World!!","day":"2020-08-16","hostname":"myhost","filter_type":"record_modifiler","append_message":"Hello Fluent Bit!!"}

ちなみに、Recordで指定したキーがすでに存在する場合は、そのキーについてはFilterのRecord Modifierの定義が無視される
ようです。

また、Record Modifilerでは「残すキー」をホワイトリスト(Whitelist_key)として定義することもできます。

[FILTER]
    Name  record_modifier
    Match *
    Whitelist_key message
    Whitelist_key day

この場合「dummy_field」は「Whitelist_key」の定義にないので、レコードから削除されます。

$ echo '{"message": "Hello World!!", "day": "2020-08-16", "dummy_field": "dummy"}' | nc localhost 5170


Aug 16 12:08:07 myhost td-agent-bit[1231]: {"date":1597579685.989803,"message":"Hello World!!","day":"2020-08-16"}

Modify

Modify Filterプラグインは、ルールを使ってレコードを変更することができます。また、変更を行うかどうかを条件で指定する
こともできます。

Modify - Fluent Bit: Official Manual

ルールというのは、こちらですね。レコードに対して、特定のキーに対する値の変更、キーと値の追加、キーのリネーム、
コピー、そして様々な指定方法でキーの削除を行うことができます。

Rules

いくつかルールを使ってみましょう。Copy(コピー)、Set(キーに対する値の変更)、Add(キーと値の追加)、
Remove(指定のキーを削除)、Remove_wildcard(指定した文字列に前方一致するキーを削除)といった感じで。

[FILTER]
    Name  modify
    Match *

    Copy  message original_message
    Set   message Hello!!
    Add   filter_type modify
    Add   hostname ${HOSTNAME}
    Remove dummy_field
    Remove_wildcard ignore_field

ルールは、以下の特徴を持ちます。

  • 先頭から適用され、複数のルールを書いた場合は前のルールの結果を元に適用される
    • つまり、ルールを入れ替えると結果が変わることがある、ということですね
  • ルールは任意の数を設定できる
  • ルールは大文字・小文字が区別されませんが、パラメーターはそうではない

ところで、ここでも環境変数が使えるようです。…そもそも設定ファイルでの書き方として、環境変数が使えるようですね。

Variables - Fluent Bit: Official Manual

では、設定した内容を確認してみます。余計なフィールドを入れたりしている、レコードを送信。

$ echo '{"message": "Hello World!!", "day": "2020-08-16", "filter_apply": "true", "dummy_field": "dummy", "ignore_field1": "dummy", "ignore_field2": "dummy", "bad_ignore_field": "dummy"}' | nc localhost 5170

結果。Remove_wildcardで指定したものは、一気になくなっています。他は、コピーや追加、変更なのでわかりやすいですね。

Aug 16 12:22:37 myhost td-agent-bit[1259]: {"date":1597580552.890862,"original_message":"Hello World!!","day":"2020-08-16","filter_apply":"true","bad_ignore_field":"dummy","message":"Hello!!","filter_type":"modify","hostname":"myhost"}

一方で、「bad_ignore_field」は残ってしまいました。このあたりが、前方一致っぽいと言っている理由ですが。

削除対象をもうちょっと柔軟に設定したい場合は、Remove_regexを使うのでしょう。

今度は、Conditionを使ってみましょう。Conditionを使うと、ルールを適用するかどうかを、レコードのキーや値に基づいて
決めることができます。

Confitions

今回は、「filter_apply」というキーに対する値が「true」、かつ「editable」というキーが存在していればルールを適用する、
という条件にしました。

[FILTER]
    Name  modify
    Match *

    Condition Key_Value_Equals filter_apply true
    Condition Key_exists editable

    Copy  message original_message
    Set   message Hello!!
    Add   filter_type modify
    Add   hostname ${HOSTNAME}
    Remove dummy_field
    Remove_wildcard ignore_field

なお、ここでいう「値」は文字列型である必要があるみたいです。

たとえば、以下は条件に一致しますが

"filter_apply": "true"

こちらは一致しません。

"filter_apply": true

では、確認してみましょう。

ルールが適用されるケース(filter_applyがtrueであり、editableもあるから)。

$  echo '{"message": "Hello World!!", "day": "2020-08-16", "filter_apply": "true", "editable": "", "dummy_field": "dummy", "ignore_field1": "dummy", "ignore_field2": "dummy", "bad_ignore_field": "dummy"}' | nc localhost 5170

結果。

Aug 16 13:01:35 myhost td-agent-bit[1351]: {"date":1597582894.641241,"original_message":"Hello World!!","day":"2020-08-16","filter_apply":"true","editable":"","bad_ignore_field":"dummy","message":"Hello!!","filter_type":"modify","hostname":"myhost"}

ルールが適用されないケース(filter_applyがtrueだが、editableがないから)。

$ echo '{"message": "Hello World!!", "day": "2020-08-16", "filter_apply": "true", "dummy_field": "dummy", "ignore_field1": "dummy", "ignore_field2": "dummy", "bad_ignore_field": "dummy"}' | nc localhost 5170

結果。

Aug 16 13:00:05 myhost td-agent-bit[1351]: {"date":1597582804.096306,"message":"Hello World!!","day":"2020-08-16","filter_apply":"true","dummy_field":"dummy","ignore_field1":"dummy","ignore_field2":"dummy","bad_ignore_field":"dummy"}

ルールが適用されないケース(filter_applyがfalseだから)。

$ echo '{"message": "Hello World!!", "day": "2020-08-16", "filter_apply": "false", "dummy_field": "dummy", "ignore_field1": "dummy", "ignore_field2": "dummy", "bad_ignore_field": "dummy"}' | nc localhost 5170

結果

Aug 16 13:00:55 myhost td-agent-bit[1351]: {"date":1597582852.588941,"message":"Hello World!!","day":"2020-08-16","filter_apply":"false","dummy_field":"dummy","ignore_field1":"dummy","ignore_field2":"dummy","bad_ignore_field":"dummy"}

Lua

最後は、Lua Filterプラグインです。Lua Filterプラグインを使うことで、独自に作成したLuaスクリプト内でレコードを変更することが
できるようになります。

Lua - Fluent Bit: Official Manual

Luaスクリプトを用意し、Filterプラグインの設定としてはスクリプトファイルのパスと、コールバックする関数を指定する
ことになります。

ところで、Lua Filterプラグインがどんな環境で動くのか?というのが気になるところなのですが、LuaJITがFluent Bitに
組み込まれているようです。

https://github.com/fluent/fluent-bit/tree/master/lib/LuaJIT-2.1.0-beta3

LuaJITは、こちら。

LuaJIT

Frequently Asked Questions (FAQ)

ドキュメントやFAQを読んでいると、LuaJITはLua 5.1と互換性があるようですね。

LuaJIT is compatible to the Lua 5.1 language standard.

Lua Filterプラグインを作成するときは、こちらを覚えておきましょう。

で、どんな感じでLuaスクリプトを書けばよいかですが、GitHubのFluent Bitリポジトリにサンプルがあるので、こちらを
参考にするとよいでしょう。

https://github.com/fluent/fluent-bit/blob/v1.5.3/scripts/test.lua

https://github.com/fluent/fluent-bit/blob/v1.5.3/scripts/append_tag.lua

https://github.com/fluent/fluent-bit/blob/v1.5.3/scripts/override_time.lua

各サンプルを見るとだいたい雰囲気がわかるのですが、ドキュメントも読んでおきましょう。

Luaスクリプトで作成するコールバック関数には、以下の3つの引数が渡ってきます。

  • レコードに関連付けられているタグ
  • タイムスタンプ
  • レコード

また、関数の戻り値としては、以下の3つを返す必要があります。

  • コード(整数)
  • タイムスタンプ
  • レコード

このコードの値がポイントで、返す値で戻り値に対する振る舞いが変わります。

  • -1 … レコードをドロップする(無視するレコードになる)
  • 0 … レコードを変更しない
  • 1 … 関数の戻り値を使って、タイムスタンプとレコードを変更できる
  • 2 … 関数の戻り値を使って、レコードを変更できる

つまり、関数の戻り値のうち、2つ目と3つ目が使われるのは、コードが1または2の時だけだということですね(2は、3つ目しか
使いませんが。

戻り値のコードで変更可能な値以外を、戻り値で変更しても結果には反映されません。

たとえば、コードが0でレコードを変更しても、レコードの変更分は無視されるということですね。

また、デバッグに関してはprint関数を使って標準出力に書き出せば良さそうです(サンプルスクリプト、test.luaがその方法を
とっています)。

では、Luaスクリプトを用意してみましょう。
/etc/td-agent-bit/scripts/my_script.lua

function cb_record_filter(tag, timestamp, record)
   local record_type = record["record_type"]

   if record_type == "drop" then
      -- -1 = レコードを破棄する
      return -1, 0, 0
   elseif record_type == "keep" then
      record["filter_type"] = "lua"
      
      -- 0 = レコードを変更しない
      return 0, 0, record
   elseif record_type == "modify" then
      local new_record = {}

      -- コピー
      for key, value in ipairs(record) do
         table.insert(new_record, key, value)
      end

      new_record["filter_type"] = "lua"

      new_record["original_message"] = record["message"]
      new_record["message"] = "Hello Lua Script!!"

      new_record["dummy_field"] = nil

      -- 1 = レコードとタイムスタンプを変更する
      -- 2 = レコードを変更し、タイムスタンプはそのまま
      return 1, timestamp, new_record
   else
      return 0, 0, 0
   end
end

「record_type」というキーの値で、レコードのドロップ、そのまま、変更の3種類を表現することにしました。

このLuaスクリプトは、/etc/td-agent-bitディレクトリ配下に置いています。

[FILTER]
    Name  lua
    Match *
    Script /etc/td-agent-bit/scripts/my_script.lua
    # Script scripts/my_script.lua
    Call   cb_record_filter

Luaスクリプトのパスは、絶対パスでも、コメントアウトにしていますが相対パスでもOKでした…。

確認してみましょう。

レコードを変更する場合。

$ echo '{"message": "Hello World!!", "day": "2020-08-21", "record_type": "modify", "dummy_field": "dummy"}' | nc localhost 5170

結果。レコードが変更されたことが確認できます。

Aug 16 13:45:06 myhost td-agent-bit[1765]: {"date":1597585504.882447,"day":"2020-08-21","record_type":"modify","original_message":"Hello World!!","message":"Hello Lua Script!!","filter_type":"lua"}

レコードを変更しない場合。

$ echo '{"message": "Hello World!!", "day": "2020-08-21", "record_type": "keep", "dummy_field": "dummy"}' | nc localhost 5170

結果。レコードの変更が無視されたことが確認できます。

Aug 16 13:47:55 myhost td-agent-bit[1792]: {"date":1597585671.897845,"message":"Hello World!!","day":"2020-08-21","record_type":"keep","dummy_field":"dummy"}

レコードをドロップする場合。

$ echo '{"message": "Hello World!!", "day": "2020-08-21", "record_type": "drop", "dummy_field": "dummy"}' | nc localhost 5170

結果は出力されません(ドロップされるので)。

これで、ざっくりと確認できたのではないかな、と思います。