Kong Pluginのソースコードの読み方入門

本記事は「Kong Advent Calendar 2024」の5日目のエントリとして、Kong Pluginのソースコードの読み方について解説する。

Kong Gatewayを触っていると、Pluginの中の動きが知りたくなることが多々ある。
例えばRequest Tranformer Pluginの[Lua拡張](https://docs.konghq.com/hub/kong-inc/request-transformer/how-to/templates/#advanced-templates)はどこでどのように展開されているのか、とか、Rate Limitting Pluginのカウンタを`local`で保存した場合、具体的にはどこに保存されるのだろう、とか。
ここではPluginのソースコードの読み方をちょろっと解説する。

※ 紹介する内容は入門的なものなので、期待は厳禁

## Pluginの基本
Pluginのコードを読む上でPluginの仕組みを知っておいた方が色々と便利なので、少しだけ紹介しておく。

Pluginは[公式ドキュメント](https://docs.konghq.com/gateway/latest/kong-plugins/#developing-custom-plugins)にもあるように、現時点ではLua、Go、Python、JavaScriptで作成できるが、Kongから提供されるPluginのほとんど（全て？）はLuaで書かれているので、Luaのコードを読んでいくことになる。
Pluginは基本的には2つのソースコードから構成される。
- `schema.lua`：プラグインのパラメータを定義したコード
- `handler.lua`：実際の処理が書かれたコード

モノによっては追加のコードもあるが、基本この2つを読んでいく形になる。
これはファイル名も固定であり、どのプラグインもこの2つは必ず持っている。

また、コードを読んでいく上で[PDK（Plugin Development Kit）](https://docs.konghq.com/gateway/latest/plugin-development/pdk/)は抑えておきたい。
Pluginの実装は基本的にこのPDKを使って実装されているので、処理を追う際は常にPDKのドキュメントを開いてPDKの意味が確認できるようにしておくと便利。

あとプラグインがリクエスト・レスポンス処理の中のどのタイミングで動いているかは[lua-nginx-moduleのDirectives](https://openresty-reference.readthedocs.io/en/latest/Directives/)を確認することになる。
例えばResponse Tranformer Pluginの`function ResponseTransformerHandler:body_filter(conf)`は`body_filter`がついているのでレスポンスのボディ処理時（`header_filter_by_lua`）に動くんだな、とか確認していく形になる。
OpenRestyやnginx-lua-moduleの仕組みを知っているとコードリーディングが捗るが、入門的なところではあまり意識しなくても大丈夫だと思うので、これについては参考程度に覚えておくとよい。


## お試しで読んでみる
ここではシンプルなコードとして、[Syslog Plugin](https://docs.konghq.com/hub/kong-inc/syslog/)をベースに解説したい。
ソースコードは[こちら](https://github.com/Kong/kong/tree/master/kong/plugins/syslog)。

### schema.lua
最初に[`schema.lua`](https://github.com/Kong/kong/blob/master/kong/plugins/syslog/schema.lua)を読んでみて、プラグインのパラメータがどのように定義されているかを確認する。
```lua
local typedefs = require "kong.db.schema.typedefs"

local severity = {
  type = "string",
  default = "info",
  required = true,
  one_of = { "debug", "info", "notice", "warning",
             "err", "crit", "alert", "emerg" }
}

local facility = { description = "The facility is used by the operating system to decide how to handle each log message.", type = "string",
  default = "user",
  required = true,
  one_of = { "auth", "authpriv", "cron", "daemon",
             "ftp", "kern", "lpr", "mail",
             "news", "syslog", "user", "uucp",
             "local0", "local1", "local2", "local3",
             "local4", "local5", "local6", "local7" },
}
```
`local xxx`部分はコンストラクタ的なもので、変数の定義みたいなものになる。
ここでプラグインのパラメータにどういう値が設定できるかを定義している。
基本的には以下の値を設定している。
- `description`：パラメータの説明
- `type`：変数の型
- `default`：デフォルト値
- `required`：パラメータが必須かどうか
- `one_of`：選択肢

他にもいくつかあるが、最低限上記くらいを抑えておけばよいと思う。

次に実際のパラメータ設定に関する項目が以下になる。
```lua
return {
  name = "syslog",
  fields = {
    { protocols = typedefs.protocols },
    { config = {
        type = "record",
        fields = {
          { log_level = severity },
          { successful_severity = severity },
          { client_errors_severity = severity },
          { server_errors_severity = severity },
          { custom_fields_by_lua = typedefs.lua_code },
          { facility = facility },
    }, }, },
  },
}
```
`return`で囲われた箇所が実際のパラメータの定義箇所となる。
パラメータの意味は以下。
- `name`：プラグイン名
- `fields`：パラメータの中身
  - `protocols`：設定できるプロトコルの一覧
  - `config`：プラグインのConfig
    - `type`: 当該カッコ内で扱うパラメータの型。`record`だと複数のフィールドを持つオブジェクトを意味する
    - `fields`：プラグインの各設定項目
 
この辺は[Syslog Pluginのパラメータ説明](https://docs.konghq.com/hub/kong-inc/syslog/configuration/#config)と見比べながら読むとより理解が深まると思う。

ちなみに`protocols`には`typedefs.protocols`を指定しているが、Syslog Pluginのサポートプロトコルはかなり多く、他のプラグインでは`typedefs.protocols_http`とか絞っていたりすることが多い。
なお、`protocols`を引っ張ってくるのに`schema.lua`の先頭で`kong.db.schema.typedefs`を引っ張ってきていたが、この仕様はPDKのドキュメントでは見つけられない。
ただ、ソースコードでは[この辺り](https://github.com/Kong/kong/blob/master/kong/db/schema/typedefs.lua#L474)になる（プロトコル一覧はもう少し掘らないと分からない）。
この辺は無理にコードを追わなくても、仕様を比較して見ていけばコードの意味が理解出来るとは思う。

こんな感じでプラグインの変数の定義がどのようになっているかは`schema.lua`を読むことで理解できる。
プラグインのドキュメントが貧弱でどのような値が設定できるかが分からない時、設定してみたものの上手くプログラム側で解釈されているのか怪しいと思った時などに`schema.lua`を読んでみるといいと思う。

### handler.lua
次に[`handler.lua`](https://github.com/Kong/kong/blob/master/kong/plugins/syslog/handler.lua)を読んでみて、実際の挙動を確認する。
`handler.lua`全体を俯瞰すると、`function SysLogHandler:log(conf)`だけ`local`がついていないことが分かる。
これがプラグインのメイン関数的なものとなる。
メイン関数的なものかどうかはコードの一番下にある、`return SysLogHandler`からも確認できる。
`handler.lua`でreturnしている関数＝メイン関数と思って問題ない。
ということでこのメイン関数的なものを最初に見てみる。
#### SysLogHandler関数
```lua:SysLogHandler
function SysLogHandler:log(conf)
  if conf.custom_fields_by_lua then
    local set_serialize_value = kong.log.set_serialize_value
    for key, expression in pairs(conf.custom_fields_by_lua) do
      set_serialize_value(key, sandbox(expression, sandbox_opts)())
    end
  end

  local message = kong.log.serialize()
  local ok, err = timer_at(0, log, conf, message)
  if not ok then
    kong.log.err("failed to create timer: ", err)
  end
end
```
まず関数名を見てみる。
`:log`というのがついているが、これは[OpenRestyのディレクティブ](https://openresty-reference.readthedocs.io/en/latest/Directives/)の`log_by_lua`で動くことを意味する。
最初のif文ではプラグインのパラメータ`custom_fields_by_lua`が設定されている場合とそうでない場合で処理が分かれている。
```lua
local set_serialize_value = kong.log.set_serialize_value
```
上記はPDKの関数呼び出しを簡略化するためにlocalの変数に関数をセットしているっぽい。
呼び出している関数の仕様は[こちら](https://docs.konghq.com/gateway/latest/plugin-development/pdk/kong.log/#konglogset_serialize_valuekey-value-options)になるが、ログのテーブルを操作する関数である。
`kong.log`に出力するログ自体を含んでいるので、`kong.log.set_serialize_value`を呼び出してこれから出力しようとしているログに変更を加えようとしていることが伺える。

次にfor文の方を見てみる。
以下で`custom_fields_by_lua`で指定した値を使ってログのテーブルに変更を加えている。
```sh
    for key, expression in pairs(conf.custom_fields_by_lua) do
      set_serialize_value(key, sandbox(expression, sandbox_opts)())
    end
```
`sandbox`はコードの冒頭で`"kong.tools.sandbox".sandbox`で設定しており、実態は[kong/kong/tools/sandbox.lua](https://github.com/Kong/kong/blob/c5199ff0c386bc8091273c8a99d6760eb382f102/kong/tools/sandbox.lua#L117)から確認できる。
`sandbox`関数のコードの説明は省略するが、`sandbox`が有効な場合は利用できるリソースを制限するものになる。
`custom_fields_by_lua`はログのフィールドを編集するパラメータであり、この値を使ってログを加工していることが読み取れる。

次の行を見てみる。
```lua
  local message = kong.log.serialize()
  local ok, err = timer_at(0, log, conf, message)
  if not ok then
    kong.log.err("failed to create timer: ", err)
  end
```
`kong.log.serialize()`は[こちら](https://docs.konghq.com/gateway/latest/plugin-development/pdk/kong.log/#konglogserialize)に仕様があるが、ログをシリアライズしてsyslogに送れるようにしているものと思われる。
次の`timer_at`はコードの先頭の方で`ngx.timer.at`をセットしており、指定した時間後に引数の関数を実行するものになる。
指定時間がゼロになってるが、これはログの書き込みが遅い場合に待たされるのを嫌がって、非同期で実行するためだけに`ngx.timer.at`を使用しているのだと推測される。

#### log関数
次に`timer_at`で呼び出した`log`関数を見てみる。
これは`SysLogHandler`関数の上くらいにlocalで用意されている。
```lua
local function log(premature, conf, message)
  if premature then
    return
  end

  if message.response.status >= 500 then
    send_to_syslog(conf.log_level, conf.server_errors_severity, message, conf.facility)

  elseif message.response.status >= 400 then
    send_to_syslog(conf.log_level, conf.client_errors_severity, message, conf.facility)

  else
    send_to_syslog(conf.log_level, conf.successful_severity, message, conf.facility)
  end
end
```
`premature`部分が謎で[lua-nginx-moduleの説明](https://github.com/openresty/lua-nginx-module?tab=readme-ov-file#ngxtimerat)を見ても分かりづらい。
どうもプロセス終了時などで呼び出されたケースなどで設定されるっぽく、普段評価されることはなさそうなのでここでは読み飛ばしても良さそう。
そこ以降は`response.status`に合わせて`send_to_syslog`関数の第2引数を変更して呼び出している。
ステータスコードにあわせてログのセベリティを変えているものだと推測できる。

#### send_to_syslog関数
最後に`send_to_syslog`関数を確認する。
```lua
local function send_to_syslog(log_level, severity, message, facility)
  if LOG_PRIORITIES[severity] <= LOG_PRIORITIES[log_level] then
    lsyslog.open(SENDER_NAME, FACILITIES[facility])
    lsyslog.log(LOG_LEVELS[severity], cjson.encode(message))
  end
end
```
Kong側のログレベル(`conf.log_level`)がログのセベリティを上回っているなら`lsyslog.open`関数と`lsyslog.log`関数を使ってログ出力する。
lsyslogはコードの冒頭で
```lua
local lsyslog = require "lsyslog"
```
で定義されており、Luaの標準的なlsyslogモジュールをロードして実行していることが分かる。
これから、ログ出力の挙動がおかしい時はlsyslogモジュールを使って再現テストすればよい、などを読み取ることが出来る。

## おわりに
本記事ではSyslog Pluginを題材にプラグインのソースの読み方を解説してみた。
Syslog Pluginは非常にシンプルなプラグインなので、他のプラグインだと労力はもっと必要になるが、基本的なアプローチは変わらない。
- Pluginの構造を理解し、関数の呼び出し順序を理解した上で`handler.lua`を中心に読んでいく
- PDKの仕様を都度確認する
- 呼び出しタイミングが分からない場合はOpenRestyのディレクティブなどを確認する

Lua自体は比較的読みやすい言語であり、上記のアプローチで追っかければ何となく分かる上、コードも大きくても1kステップ程度で終わることが多いので、構えずに気軽に読むとよいかと思う。
あとカスタムプラグインを作りたい場合、既存のプラグインを参考に実装した方が楽なので、カスタムプラグインを作る際はまずは適当にコードを読んでみることをオススメする。