この記事は ESM Advent Calendar 2022 23日目の記事です。
背景
仕事で初めてFluentdを使っています。色々と勉強になったのでメモがてら記事を残します。
アプリケーションから出力したログをFluentdで収集し、整形してから他のデータレイクに転送するということをやっているのですが、データレイク側のインターフェースが変更になり文字列化されたJSONの中から特定のキー項目を抜き出しつつ、抜き出したキー項目はJSONの中から消すという要件が出てきたため、なんとかFluentdだけで頑張れないか調べました。
自分でも何言ってるか訳分からなくなってきたので以下にイメージを書きます(笑)
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
"message": "{\"inner\":{\"innerKey1\":\"innerValue1\",\"innerKey2\":\"innerValue2\",\"innerKey3\":\"innerValue3\"}}",
"key4": "value4",
"key5": "value5"
}
messageの中身はJSONを文字列化したものです。このJSON文字列の中にあるinnerKey3を外側に移動させるイメージです。
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
"message": "{\"inner\":{\"innerKey1\":\"innerValue1\",\"innerKey2\":\"innerValue2\"}}",
"key4": "value4",
"key5": "value5",
"innerKey3": "innerValue3"
}
ちなみにこのmessage->innerのJSON文字列の中身は一定ではなくログの種類によって中身はバラバラですが、innerKey3は必ずmessage->innerの中のどこかには存在するという前提です。
やったこと
JSONの中から特定のキー項目を抽出
以下の公式ページの通り、filterのrecord_transformerを使えば出来そうだったのでやってみました。
NGな例
以下のような設定を書いてみましたがダメでした。
<filter hoge>
@type record_transformer
enable_ruby
<record>
innerKey3 ${record["message"]["inner"]["innerKey3"]}
</record>
</filter>
理由はmessageがJSONオブジェクトではなくて文字列になっているから。
OKな例
文字列でダメならJSON化すればできるはず。ということで以下の公式ページの通りJSON化をまずやります。
<filter hoge>
@type parser
key_name message
reserve_data true
<parse>
@type json
</parse>
</filter>
そうすると以下のような構造になります。
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
"message": {
"inner": {
"innerKey1": "innerValue1",
"innerKey2": "innerValue2",
"innerKey3": "innerValue3"
}
},
"key4": "value4",
"key5": "value5"
}
構造変換後に先程のFilterを設定したら動きました!
<filter hoge>
@type record_transformer
enable_ruby
<record>
innerKey3 ${record["message"]["inner"]["innerKey3"]}
</record>
</filter>
// ----- こんな書き方でもOK -----
<filter hoge>
@type record_transformer
enable_ruby
<record>
innerKey3 ${record.dig("message", "inner", "innerKey3")}
</record>
</filter>
ちなみにFilterを複数設定した場合は上から順番に実行されるので、数珠つなぎで何個でも設定できます。たぶん。このFilterを通すと結果は以下のようになりました。
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
"message": {
"inner": {
"innerKey1": "innerValue1",
"innerKey2": "innerValue2",
"innerKey3": "innerValue3"
}
},
"key4": "value4",
"key5": "value5",
"innerKey3": "innerValue3"
}
めでたく message->inner->innerKey3 を外側にコピーすることができました。
JSONの中から特定のキー項目を削除
キー項目の削除は、JSON構造化されていれば以下の公式ページを参考に簡単に実現できます。
<filter hoge>
@type record_transformer
remove_keys $.message.inner.innerKey3
</filter>
このFilterを通すと結果はこうなりました。
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
"message": {
"inner": {
"innerKey1": "innerValue1",
"innerKey2": "innerValue2"
}
},
"key4": "value4",
"key5": "value5",
"innerKey3": "innerValue3"
}
message->inner の中から innerKey3 が消えましたね!
再度JSON構造を文字列化
キー項目を外側に出せた時点で出来た!と思ったのですがそのままではデータレイクに連携できません。理由は、データレイク側ではmessageを文字列で受け付けているからです。ということで再度messageを文字列化します。
<filter hoge>
@type record_transformer
<record>
message ${JSON.generate(record["message"])}
</record>
</filter>
このFilterを通してようやく目指すべき形になりました。
{
"key1": "value1",
"key2": "value2",
"key3": "value3",
"message": "{\"inner\":{\"innerKey1\":\"innerValue1\",\"innerKey2\":\"innerValue2\"}}",
"key4": "value4",
"key5": "value5",
"innerKey3": "innerValue3"
}
事の顛末
Fluentd側だけでなんとか頑張れることは分かったのですが、Fluentdで頑張りすぎて複雜になってしまうよりはアプリケーション側のログのフォーマットを変更する方向に方針が倒れたので、実際にはこの案は採用されずにボツになりました(笑)
とは言えFluentdの勉強になったので最後に分かったことを纏めて終わります。
- Filterは数珠つなぎで順番に実行できる。
- FluentdはRubyで出来ているので
enable_ruby
と書くことでRubyの表現が使える。- digとかJSON.generateとかもそう。
- Rubyが書ければ色々なことができそう。
- ただし私はRubyのことはよく知らない。悲しい。
- 公式サイトのドキュメントが結構充実している
以上です。ありがとうございました。