本記事は「Kong Advent Calendar 2024」の5日目のエントリとして、Kong Pluginのソースコードの読み方について解説する。
Kong Gatewayを触っていると、Pluginの中の動きが知りたくなることが多々ある。
例えばRequest Tranformer PluginのLua拡張はどこでどのように展開されているのか、とか、Rate Limitting Pluginのカウンタをlocal
で保存した場合、具体的にはどこに保存されるのだろう、とか。
ここではPluginのソースコードの読み方をちょろっと解説する。
※ 紹介する内容は入門的なものなので、期待は厳禁
Pluginの基本
Pluginのコードを読む上でPluginの仕組みを知っておいた方が色々と便利なので、少しだけ紹介しておく。
Pluginは公式ドキュメントにもあるように、現時点ではLua、Go、Python、JavaScriptで作成できるが、Kongから提供されるPluginのほとんど(全て?)はLuaで書かれているので、Luaのコードを読んでいくことになる。
Pluginは基本的には2つのソースコードから構成される。
-
schema.lua
:プラグインのパラメータを定義したコード -
handler.lua
:実際の処理が書かれたコード
モノによっては追加のコードもあるが、基本この2つを読んでいく形になる。
これはファイル名も固定であり、どのプラグインもこの2つは必ず持っている。
また、コードを読んでいく上でPDK(Plugin Development Kit)は抑えておきたい。
Pluginの実装は基本的にこのPDKを使って実装されているので、処理を追う際は常にPDKのドキュメントを開いてPDKの意味が確認できるようにしておくと便利。
あとプラグインがリクエスト・レスポンス処理の中のどのタイミングで動いているかはlua-nginx-moduleのDirectivesを確認することになる。
例えばResponse Tranformer Pluginのfunction ResponseTransformerHandler:body_filter(conf)
はbody_filter
がついているのでレスポンスのボディ処理時(header_filter_by_lua
)に動くんだな、とか確認していく形になる。
OpenRestyやnginx-lua-moduleの仕組みを知っているとコードリーディングが捗るが、入門的なところではあまり意識しなくても大丈夫だと思うので、これについては参考程度に覚えておくとよい。
お試しで読んでみる
ここではシンプルなコードとして、Syslog Pluginをベースに解説したい。
ソースコードはこちら。
schema.lua
最初にschema.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
:選択肢
他にもいくつかあるが、最低限上記くらいを抑えておけばよいと思う。
次に実際のパラメータ設定に関する項目が以下になる。
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のパラメータ説明と見比べながら読むとより理解が深まると思う。
ちなみにprotocols
にはtypedefs.protocols
を指定しているが、Syslog Pluginのサポートプロトコルはかなり多く、他のプラグインではtypedefs.protocols_http
とか絞っていたりすることが多い。
なお、protocols
を引っ張ってくるのにschema.lua
の先頭でkong.db.schema.typedefs
を引っ張ってきていたが、この仕様はPDKのドキュメントでは見つけられない。
ただ、ソースコードではこの辺りになる(プロトコル一覧はもう少し掘らないと分からない)。
この辺は無理にコードを追わなくても、仕様を比較して見ていけばコードの意味が理解出来るとは思う。
こんな感じでプラグインの変数の定義がどのようになっているかはschema.lua
を読むことで理解できる。
プラグインのドキュメントが貧弱でどのような値が設定できるかが分からない時、設定してみたものの上手くプログラム側で解釈されているのか怪しいと思った時などにschema.lua
を読んでみるといいと思う。
handler.lua
次にhandler.lua
を読んでみて、実際の挙動を確認する。
handler.lua
全体を俯瞰すると、function SysLogHandler:log(conf)
だけlocal
がついていないことが分かる。
これがプラグインのメイン関数的なものとなる。
メイン関数的なものかどうかはコードの一番下にある、return SysLogHandler
からも確認できる。
handler.lua
でreturnしている関数=メイン関数と思って問題ない。
ということでこのメイン関数的なものを最初に見てみる。
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のディレクティブのlog_by_lua
で動くことを意味する。
最初のif文ではプラグインのパラメータcustom_fields_by_lua
が設定されている場合とそうでない場合で処理が分かれている。
local set_serialize_value = kong.log.set_serialize_value
上記はPDKの関数呼び出しを簡略化するためにlocalの変数に関数をセットしているっぽい。
呼び出している関数の仕様はこちらになるが、ログのテーブルを操作する関数である。
kong.log
に出力するログ自体を含んでいるので、kong.log.set_serialize_value
を呼び出してこれから出力しようとしているログに変更を加えようとしていることが伺える。
次にfor文の方を見てみる。
以下でcustom_fields_by_lua
で指定した値を使ってログのテーブルに変更を加えている。
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から確認できる。
sandbox
関数のコードの説明は省略するが、sandbox
が有効な場合は利用できるリソースを制限するものになる。
custom_fields_by_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()
はこちらに仕様があるが、ログをシリアライズしてsyslogに送れるようにしているものと思われる。
次のtimer_at
はコードの先頭の方でngx.timer.at
をセットしており、指定した時間後に引数の関数を実行するものになる。
指定時間がゼロになってるが、これはログの書き込みが遅い場合に待たされるのを嫌がって、非同期で実行するためだけにngx.timer.at
を使用しているのだと推測される。
log関数
次にtimer_at
で呼び出したlog
関数を見てみる。
これはSysLogHandler
関数の上くらいにlocalで用意されている。
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の説明を見ても分かりづらい。
どうもプロセス終了時などで呼び出されたケースなどで設定されるっぽく、普段評価されることはなさそうなのでここでは読み飛ばしても良さそう。
そこ以降はresponse.status
に合わせてsend_to_syslog
関数の第2引数を変更して呼び出している。
ステータスコードにあわせてログのセベリティを変えているものだと推測できる。
send_to_syslog関数
最後にsend_to_syslog
関数を確認する。
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はコードの冒頭で
local lsyslog = require "lsyslog"
で定義されており、Luaの標準的なlsyslogモジュールをロードして実行していることが分かる。
これから、ログ出力の挙動がおかしい時はlsyslogモジュールを使って再現テストすればよい、などを読み取ることが出来る。
おわりに
本記事ではSyslog Pluginを題材にプラグインのソースの読み方を解説してみた。
Syslog Pluginは非常にシンプルなプラグインなので、他のプラグインだと労力はもっと必要になるが、基本的なアプローチは変わらない。
- Pluginの構造を理解し、関数の呼び出し順序を理解した上で
handler.lua
を中心に読んでいく - PDKの仕様を都度確認する
- 呼び出しタイミングが分からない場合はOpenRestyのディレクティブなどを確認する
Lua自体は比較的読みやすい言語であり、上記のアプローチで追っかければ何となく分かる上、コードも大きくても1kステップ程度で終わることが多いので、構えずに気軽に読むとよいかと思う。
あとカスタムプラグインを作りたい場合、既存のプラグインを参考に実装した方が楽なので、カスタムプラグインを作る際はまずは適当にコードを読んでみることをオススメする。