後端新手如何從 0 到 1 打造一款 Apache APISIX 外掛

Apache_APISIX發表於2022-02-16

在過去的幾個月,社群使用者為 Apache APISIX 新增了許多外掛,豐富了 Apache APISIX 的生態。從使用者的角度而言,更多樣化的外掛出現無疑是一件好事,它們在完善 Apache APISIX 高效能和低延遲的基礎之上,滿足了使用者對於閘道器的更多期望,即 “一站式”和“多功能”。

社群的貢獻者們是如何為 Apache APISIX 開發外掛的呢?Apache APISIX 部落格上的文章似乎都沒有詳細講述過開發外掛的流程。那麼這次我們換一個視角,從外掛開發者的角度出發,一起來看看一款外掛誕生的全過程吧!

本篇文章記錄了一個沒有後端經驗的前端工程師開發 file-logger 外掛的過程。在詳細說明實現過程之前,先向大家簡單介紹下 file-logger 的功能。

功能介紹

file-logger 支援使用 Apache APISIX 外掛後設資料生成自定義的日誌格式,使用者可以通過 file-logger 外掛將 JSON 格式的請求和響應資料附加到日誌檔案中,也可以將 Log 資料流推送到指定位置。

試想,在監控某一個路由的訪問日誌時,很多時候我們關注的不僅是某些請求和響應資料的值,還希望將日誌資料單獨寫入到指定的檔案中。這時就可以使用 file-logger 外掛來幫忙實現這些需求。

不支援在 Docs 外貼上 block

具體實現過程中我們可以通過 file-logger 將日誌資料單獨寫入指定的日誌檔案,簡化監控和除錯的過程。

開發實現過程

介紹完 file-logger 的功能之後,大家也算對這個外掛有了更多的認識。下面為大家詳細講解一下,沒有服務端經驗的我是如何從 0 開始為 Apache APISIX 完成該外掛並新增相應測試的。

確定外掛名稱及優先順序

開啟 Apache APISIX 外掛開發指南,按照先後順序需要確定以下幾項:

  1. 確定外掛分類。
  2. 確定外掛優先順序,並更新 conf/config-default.yaml 檔案。

因為此次開發的 file-logger 屬於日誌型別的外掛,所以我參考了Apache APISIX 已有的日誌外掛的名稱和排序,將 file-logger 放到了這裡:

img

諮詢了其他外掛的作者和社群的熱心成員之後,最終確認了該外掛的名稱 file-logger 和優先順序 399

需要注意,外掛的優先順序與執行順序有關,優先順序的值越大,執行越靠前。而外掛名稱的排序與執行順序無關。

建立最小可執行外掛檔案

確認好外掛名稱和優先順序後,便可在 apisix/plugins/ 目錄下建立我們的外掛程式碼檔案,這裡有兩點需要注意下:

  1. 如果是直接在 apisix/plugins/ 目錄下直接建立外掛程式碼檔案,就無需更改 Makefile 檔案。
  2. 如果你的外掛有新建自己的程式碼目錄,那麼就需要更新 Makefile 檔案,詳細步驟可參考 Apache APISIX 外掛開發指南
  3. 下面我們在 apisix/plugins/ 目錄下建立 file-logger.lua 檔案。
  4. 然後根據官方給出的 example-plugin 的例子做參考,來完成一個初始化版本。
-- 在頭部引入我們所需要的模組
local core         =   require("apisix.core")
local plugin       =   require("apisix.plugin")
local ngx          =   ngx

-- 宣告外掛名稱
local plugin_name = "file-logger"

-- 定義外掛 schema 格式
local schema = {
    type = "object",
    properties = {
        path = {
            type = "string"
        },
    },
    required = {"path"}
}

-- 外掛後設資料 schema
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

通過 example-plugin 的例子完成了一個最小的可用外掛檔案後,便可通過 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 的狀態碼,表明已成功建立了路由。

  1. 執行 curl 命令向該路由傳送請求,測試 file-logeer 外掛是否已經啟動。
$ curl -i http://127.0.0.1:9080/api/hello
HTTP/1.1 200 OK
...
hello, world
  1. logs/error.log 檔案中會有一條這樣的記錄:

img

可以看到, 在 conf 引數中我們為外掛配置的 path: logs/file.log 已經成功的儲存了。到此我們已經成功建立了一個最小可用的外掛,在日誌階段列印了 confctx 引數的資料。

之後,我們可以直接在該 file-logger.lua 外掛程式碼檔案中,為它編寫核心功能。這裡我們可以直接執行 apisix reload 命令來重新載入最新的外掛程式碼,而無需重啟 Apache APISIX。

為 file-logger 外掛編寫核心功能

file-logger 外掛的主要功能是寫日誌資料。在經過詢問和查閱資料後,我瞭解到 Lua 的 IO 庫:https://www.tutorialspoint.co...,於是確認了該外掛的功能邏輯大致為以下幾步:

  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
  1. 參考 http-logger 外掛原始碼,完成了將日記資料傳給寫日誌資料的方法和 metadata 的一些判斷和處理。
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 外掛,並且配置了 pathlogs/file.log,所以此時我們只需給測試路由傳送請求,以驗證日誌收集的結果:

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

在對應的 logs/file.log 中我們可以看到每條記錄都是以 JSON 的格式儲存下來。將其中一條資料格式化之後如下所示:

{
    "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__ 分為序言部分和資料部分。由於目前官網測試相關文件沒有明確歸檔分類,更多具體內容可參考文末的相關資料。下面我為大家列出參考相關資料後完成的其中一個測試用例:

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

至此外掛新增測試環節結束。

總結

以上就是我作為一個後端新手,從 0 開始實現一款 Apache APISIX 外掛的全過程。在開發外掛的過程中確實碰到了很多坑,比較幸運的是 Apache APISIX 社群裡面有很多熱心的大佬幫我解惑,使得 file-logger 外掛的開發和測試全程都比較順暢。如果你對這個外掛感興趣,或想要檢視外掛詳情, 可以參考 Apache APISIX 官方文件

目前,Apache APISIX 也在開發其他外掛以支援整合更多服務,如果您對此感興趣,歡迎隨時在 GitHub Discussion 中發起討論,也可通過 郵件列表 進行交流討論。

相關文章