4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KongAdvent Calendar 2024

Day 5

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

Last updated at Posted at 2024-12-04

本記事は「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関数

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ステップ程度で終わることが多いので、構えずに気軽に読むとよいかと思う。
あとカスタムプラグインを作りたい場合、既存のプラグインを参考に実装した方が楽なので、カスタムプラグインを作る際はまずは適当にコードを読んでみることをオススメする。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?