FluentdでJSON文字列の中から特定のキー項目を抜き出して消す

この記事は ESM Advent Calendar 2022 23日目の記事です。

https://adventar.org/calendars/8218

# 背景

仕事で初めてFluentdを使っています。色々と勉強になったのでメモがてら記事を残します。

アプリケーションから出力したログをFluentdで収集し、整形してから他のデータレイクに転送するということをやっているのですが、データレイク側のインターフェースが変更になり文字列化されたJSONの中から特定のキー項目を抜き出しつつ、抜き出したキー項目はJSONの中から消すという要件が出てきたため、なんとかFluentdだけで頑張れないか調べました。

自分でも何言ってるか訳分からなくなってきたので以下にイメージを書きます（笑）

```json:元々の形
{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3",
  "message": "{\"inner\":{\"innerKey1\":\"innerValue1\",\"innerKey2\":\"innerValue2\",\"innerKey3\":\"innerValue3\"}}",
  "key4": "value4",
  "key5": "value5"
}
```

messageの中身はJSONを文字列化したものです。このJSON文字列の中にあるinnerKey3を外側に移動させるイメージです。

```json:目指すべき形
{
  "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を使えば出来そうだったのでやってみました。

https://docs.fluentd.org/filter/record_transformer

### NGな例

以下のような設定を書いてみましたがダメでした。

```
<filter hoge>
  @type record_transformer
  enable_ruby
  <record>
    innerKey3 ${record["message"]["inner"]["innerKey3"]}
  </record>
</filter>
```

理由はmessageがJSONオブジェクトではなくて文字列になっているから。

### OKな例

文字列でダメならJSON化すればできるはず。ということで以下の公式ページの通りJSON化をまずやります。

https://docs.fluentd.org/filter/parser#reserve_data

```:Filter①
<filter hoge>
  @type parser
  key_name message
  reserve_data true
  <parse>
    @type json
  </parse>
</filter>
```

そうすると以下のような構造になります。

```json
{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3",
  "message": {
    "inner": {
      "innerKey1": "innerValue1",
      "innerKey2": "innerValue2",
      "innerKey3": "innerValue3"
    }
  },
  "key4": "value4",
  "key5": "value5"
}
```

構造変換後に先程のFilterを設定したら動きました！

```: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を通すと結果は以下のようになりました。

```json
{
  "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構造化されていれば以下の公式ページを参考に簡単に実現できます。

https://docs.fluentd.org/filter/record_transformer#remove_keys

```:Filter③
<filter hoge>
  @type record_transformer
  remove_keys $.message.inner.innerKey3
</filter>
```

このFilterを通すと結果はこうなりました。

```json
{
  "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④
<filter hoge>
  @type record_transformer
  <record>
    message ${JSON.generate(record["message"])}
  </record>
</filter>
```

このFilterを通してようやく目指すべき形になりました。

```json
{
  "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のことはよく知らない。悲しい。
- 公式サイトのドキュメントが結構充実している
  - https://docs.fluentd.org/

以上です。ありがとうございました。