Apache APISIXプラグインをゼロから作る方法は?

Qi Guo

Qi Guo

February 16, 2022

Ecosystem

過去数ヶ月間、コミュニティユーザーは多くのプラグインをApache APISIXに追加し、Apache APISIXのエコシステムを豊かにしてきました。ユーザーの視点から見ると、より多様なプラグインの出現は確かに良いことであり、Apache APISIXの高性能と低遅延を完璧にした上で、ユーザーが「ワンストップ」で「多機能」なゲートウェイに対する期待をさらに満たしています。

Apache APISIXのブログ記事では、プラグイン開発のプロセスについて詳しく説明しているものはないようです。そこで、プラグイン開発者の視点からプロセスを見て、プラグインがどのように生まれるのかを見てみましょう!

この記事では、バックエンド経験のないフロントエンドエンジニアfile-loggerプラグインを開発するプロセスを記録しています。実装プロセスの詳細に入る前に、file-loggerの機能を簡単に紹介します。

file-loggerプラグインの紹介

file-loggerは、Apache APISIXプラグインメタデータを使用してカスタムログフォーマットを生成することをサポートしています。ユーザーはfile-loggerプラグインを介して、リクエストとレスポンスのデータをJSON形式でログファイルに追加したり、ログデータストリームを指定された場所にプッシュしたりできます。

想像してみてください:ルートのアクセスログを監視する際に、特定のリクエストとレスポンスデータの値だけでなく、ログデータを指定されたファイルに書き込みたい場合があります。このような場合にfile-loggerプラグインを使用して、これらの目標を達成することができます。

how it works

file-loggerを使用して、ログデータを特定のログファイルに書き込むことで、監視とデバッグのプロセスを簡素化できます。

プラグインを実装する方法は?

file-loggerの機能を紹介した後、このプラグインについてより理解が深まったことでしょう。以下は、サーバーサイド経験のないフロントエンド開発者である私が、Apache APISIXのプラグインを開発し、それに対応するテストを追加する方法についての詳細な説明です。

プラグインの名前と優先順位を確認する

Apache APISIXプラグイン開発ガイドを開き、優先順位に従って以下の2つのことを決定する必要があります:

  1. プラグインのカテゴリを決定する。
  2. プラグインの優先順位を決定し、conf/config-default.yamlファイルを更新する。

今回のfile-logger開発はロギングタイプのプラグインであるため、既存のApache APISIXのロギングプラグインの名前と順序を参考にし、file-loggerを配置します。

file-logger's position

他のプラグイン作者やコミュニティの熱心なメンバーと相談した後、プラグインの名前file-loggerと優先順位399が最終的に確認されました。

注意:プラグインの優先順位は実行順序に関連しており、優先順位の値が高いほど、実行順序が前になります。また、プラグイン名の順序は実行順序とは関係ありません。

最小限の実行可能なプラグインファイルを作成する

プラグイン名と優先順位を確認した後、apisix/plugins/ディレクトリにプラグインコードファイルを作成できます。ここで注意すべき点が2つあります:

  • プラグインコードファイルをapisix/plugins/ディレクトリに直接作成する場合、Makefileファイルを変更する必要はありません。
  • プラグインに独自のコードディレクトリがある場合、Makefileファイルを更新する必要があります。詳細な手順については、Apache APISIXプラグイン開発ガイドを参照してください。
  1. ここでは、apisix/plugins/ディレクトリにfile-logger.luaファイルを作成します。
  2. 次に、example-pluginに基づいて初期化バージョンを完成させます。
-- ヘッダーで必要なモジュールを導入
local log_util     =   require("apisix.utils.log-util")
local core         =   require("apisix.core")
local plugin       =   require("apisix.plugin")
local ngx          =   ngx

-- プラグインの名前を宣言
local plugin_name = "file-logger"

-- プラグインのスキーマフォーマットを定義
local schema = {
    type = "object",
    properties = {
        path = {
            type = "string"
        },
    },
    required = {"path"}
}

-- プラグインメタデータスキーマ
local metadata_schema = {
    type = "object",
    properties = {
        log_format = log_util.metadata_schema_log_format
    }
}

local _M = {
    version = 0.1,
    priority = 399,
    name = plugin_name,
    schema = schema,
    metadata_schema = metadata_schema
}

-- プラグイン設定が正しいか確認
function _M.check_schema(conf, schema_type)
    if schema_type == core.schema.TYPE_METADATA then
        return core.schema.check(metadata_schema, conf)
    end
    return core.schema.check(schema, conf)
end

-- ログフェーズ
function _M.log(conf, ctx)
    core.log.warn("conf: ", core.json.encode(conf))
    core.log.warn("ctx: ", core.json.encode(ctx, true))
end

return _M

最小限の使用可能なプラグインファイルが準備できたら、core.log.warn(core.json.encode(conf))core.log.warn("ctx: ", core.json.encode(ctx, true))を介して、プラグインの設定データとリクエスト関連データをerror.logファイルに出力できます。

プラグインを有効にしてテストする

以下はテストのためのいくつかのステップです。プラグインが設定したプラグインデータとリクエスト関連データ情報をエラーログファイルに正常に出力できるかどうかをテストするために、プラグインを有効にし、テストルートを作成する必要があります。

  1. ローカルにテスト用のアップストリームを準備します(この記事で使用したテストアップストリームは127.0.0.1:3030/api/helloで、ローカルで作成しました)。

  2. curlコマンドを使用してルートを作成し、新しいプラグインを有効にします。

    curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
    "plugins": {
        "file-logger": {
        "path": "logs/file.log"
        }
    },
    "upstream": {
        "type": "roundrobin",
        "nodes": {
        "127.0.0.1:3030": 1
        }
    },
    "uri": "/api/hello"
    }'
    

    その後、ステータスコード200が表示され、ルートが正常に作成されたことを示します。

  3. curlコマンドを実行してルートにリクエストを送信し、file-loggerプラグインが起動したかどうかをテストします。

    curl -i http://127.0.0.1:9080/api/hello
    HTTP/1.1 200 OK
    ...
    hello, world
    
  4. logs/error.logファイルに記録が表示されます:

    record in logs/error.log

    ご覧の通り、プラグインに設定したpath: logs/file.logが正常に保存されています。この時点で、ログフェーズでconfctxパラメータを出力する最小限の使用可能なプラグインを正常に作成しました。

    その後、file-logger.luaプラグインのコア機能を直接コードファイルに記述できます。ここでは、Apache APISIXを再起動せずに、apisix reloadコマンドを実行して最新のプラグインコードをリロードできます。

file-loggerプラグインのコア機能を記述する

file-loggerプラグインの主な機能は、ログデータを書き込むことです。コミュニティの他の人々に尋ねたり、情報を確認したりした後、LuaのIOライブラリについて学び、プラグインの機能のロジックがおおよそ以下のステップであることを確認しました。

  1. 各リクエストを受け取った後、プラグインに設定されたpathにログデータを出力します。

    1. まず、ログフェーズでconfを介してfile-loggerpathの値を取得します。
    2. 次に、LuaのIOライブラリを使用して、ファイルを作成、開き、書き込み、キャッシュをリフレッシュし、ファイルを閉じます。
  2. ファイルのオープン失敗、ファイルの作成失敗などのエラーを処理します。

    local function write_file_data(conf, log_message)
        local msg, err = core.json.encode(log_message)
        if err then
            return core.log.error("message json serialization failed, error info : ", err)
        end
    
        local file, err = io_open(conf.path, 'a+')
    
        if not file then
            core.log.error("failed to open file: ", conf.path, ", error info: ", err)
        else
            local ok, err = file:write(msg, '\n')
            if not ok then
                core.log.error("failed to write file: ", conf.path, ", error info: ", err)
            else
                file:flush()
            end
            file:close()
        end
    end
    
  3. http-loggerプラグインのソースコードを参考にして、ログデータを書き込むためのメソッドと、メタデータのいくつかの判断と処理を完了しました。

    function _M.log(conf, ctx)
        local metadata = plugin.plugin_metadata(plugin_name)
        local entry
    
        if metadata and metadata.value.log_format
            and core.table.nkeys(metadata.value.log_format) > 0
        then
            entry = log_util.get_custom_format_log(ctx, metadata.value.log_format)
        else
            entry = log_util.get_full_log(ngx, conf)
        end
    
        write_file_data(conf, entry)
    end
    

検証とテストの追加

ログ記録を検証する

テストルートを作成した際にfile-loggerプラグインを有効にし、パスをlogs/file.logに設定したため、テストルートにリクエストを送信してログ収集の結果を検証できます。

curl -i http://127.0.0.1:9080/api/hello

対応するlogs/file.logでは、各レコードがJSON形式で保存されていることがわかります。データの1つをフォーマットすると、以下のようになります。

{
  "server": {
    "hostname": "....",
    "version": "2.11.0"
  },
  "client_ip": "127.0.0.1",
  "upstream": "127.0.0.1:3030",
  "route_id": "1",
  "start_time": 1641285122961,
  "latency": 13.999938964844,
  "response": {
    "status": 200,
    "size": 252,
    "headers": {
      "server": "APISIX/2.11.0",
      "content-type": "application/json; charset=utf-8",
      "date": "Tue, 04 Jan 2022 08:32:02 GMT",
      "vary": "Accept-Encoding",
      "content-length": "19",
      "connection": "close",
      "etag": "\"13-5j0ZZR0tI549fSRsYxl8c9vAU78\""
    }
  },
  "service_id": "",
  "request": {
    "querystring": {},
    "size": 87,
    "method": "GET",
    "headers": {
      "host": "127.0.0.1:9080",
      "accept": "*/*",
      "user-agent": "curl/7.77.0"
    },
    "url": "http://127.0.0.1:9080/api/hello",
    "uri": "/api/hello"
  }
}

これでログ記録の検証は完了です。検証結果は、プラグインが正常に起動し、適切なデータを返したことを示しています。

プラグインのためのテストを追加する

add_block_preprocessor部分のコードについては、Perlの経験がなかったため、最初に書き始めた時は混乱しました。調査した後、正しい使用方法を理解しました:データセクションでrequestアサーションとno_error_logアサーションを記述しない場合、デフォルトのアサーションは以下のようになります。

--- request
GET /t
--- no_error_log
[error]

他のロギングテストファイルを参考にした後、t/plugin/ディレクトリにfile-logger.tファイルを作成しました。

各テストファイルは**DATA**によって前文セクションとデータセクションに分かれています。公式サイトにはテスト関連のドキュメントの明確な分類がないため、詳細については記事の最後にある関連資料を参照してください。以下は、関連資料を参考にして完成したテストケースの1つです。

use t::APISIX 'no_plan';

no_long_string();
no_root_location();

add_block_preprocessor(sub {
    my ($block) = @_;

    if (! $block->request) {
        $block->set_value("request", "GET /t");
    }

    if (! $block->no_error_log && ! $block->error_log) {
        $block->set_value("no_error_log", "[error]");
    }
});

run_tests;

__DATA__

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            local configs = {
                -- full configuration
                {
                    path = "file.log"
                },
                -- property "path" is required
                {
                    path = nil
                }
            }

            local plugin = require("apisix.plugins.file-logger")

            for i = 1, #configs do
                ok, err = plugin.check_schema(configs[i])
                if err then
                    ngx.say(err)
                else
                    ngx.say("done")
                end
            end
        }
    }
--- response_body_like
done
property "path" is required

これでプラグイン追加テストセッションは終了です。

まとめ

以上が、バックエンド初心者としてApache APISIXプラグインを0から実装する全プロセスです。プラグイン開発の過程で多くの問題に遭遇しましたが、幸いにもApache APISIXコミュニティには多くの熱心な仲間がいて、問題を解決するのを手伝ってくれたため、file-loggerプラグインの開発とテストは全体的にスムーズに進みました。このプラグインに興味がある場合や、プラグインの詳細を見たい場合は、公式Apache APISIXドキュメントを参照してください。

Apache APISIXは現在も他のプラグインを開発しており、より多くの統合サービスをサポートする予定です。興味がある場合は、GitHub Discussionでディスカッションを開始するか、メーリングリストを通じてお問い合わせください。

参考文献

Tags: