How did a Beginner Build an Apache APISIX Plugin from 0 to 1

Qi Guo

Update At 2/16/2022

Over the past few months, community users have added many plugins to Apache APISIX, enriching the Apache APISIX ecosystem. From the user's point of view, the emergence of more diverse plugins is certainly a good thing, as they fulfill more of the user's expectations for a gateway that is "one-stop" and "multi-functional" processor on top of perfecting the high performance and low latency of Apache APISIX.

None of the articles on the Apache APISIX blog seem to go into detail about the process of developing plugins. So let's take a look at the process from the perspective of a plugin developer and see how a plugin is born!

This article documents the process of developing the file-logger plugin by a front-end engineer with no back-end experience. Before digging into the details of the implementation process, we will briefly introduce the functionality of file-logger.

Introduction of file-logger plugin

file-logger supports generating custom log formats using Apache APISIX plugin metadata. Users can append request and response data in JSON format to log files via the file-logger plugin, or push the log data stream to a specified location.

Imagine this: when monitoring the access log of a route, we not only care about the value of certain request and response data, but also want to write the log data to a specified file. This is where the file-logger plugin can be used to help achieve these goals.

how it works

We can use file-logger to write log data to a specific log file to simplify the process of monitoring and debugging.

How to implement a plugin?

After introducing the features of file-logger, you will have a better understanding of this plugin. The following is a detailed explanation of how I, a front-end developer with no server-side experience, develop the plugin for Apache APISIX and add the corresponding tests for it.

Confirm the name and priority of the plugin

Open the Apache APISIX Plugin Development Guide and in order of priority you need to determine the following two things:

  1. Determine the plugin category.
  2. prioritize the plugins and update the conf/config-default.yaml file.

Since this development of file-logger is a logging type plugin, I refer to the name and ordering of the existing logging plugins for Apache APISIX and place file-logger here.

file-logger's position

After consulting with other plugin authors and enthusiastic members of the community, the name file-logger and priority 399 of the plugin were finally confirmed.

Note that the priority of the plugin is related to the order of execution; the higher the value of the priority, the more forward the execution. And the ordering of plugin names is not related to the order of execution.

Create a minimum executable plugin file

After confirming the plugin name and priority, you can create our plugin code file in apisix/plugins/ directory . There are two points to note here:

  • If the plugin code file is created directly in the apisix/plugins/ directory, there is no need to change the Makefile file.
  • If your plugin has its own code directory, you need to update the Makefile file, please refer to the Apache APISIX Plugin Development Guide for detailed steps.
  1. Here we create the file-logger.lua file in the apisix/plugins/ directory.
  2. Then we will complete an initialized version based on the example-plugin.
1-- Introduce the module we need in the header
2local log_util     =   require("apisix.utils.log-util")
3local core         =   require("apisix.core")
4local plugin       =   require("apisix.plugin")
5local ngx          =   ngx
6
7-- Declare the plugin's name
8local plugin_name = "file-logger"
9
10-- Define the plugin schema format
11local schema = {
12    type = "object",
13    properties = {
14        path = {
15            type = "string"
16        },
17    },
18    required = {"path"}
19}
20
21-- Plugin metadata schema
22local metadata_schema = {
23    type = "object",
24    properties = {
25        log_format = log_util.metadata_schema_log_format
26    }
27}
28
29
30local _M = {
31    version = 0.1,
32    priority = 399,
33    name = plugin_name,
34    schema = schema,
35    metadata_schema = metadata_schema
36}
37
38-- Check if the plugin configuration is correct
39function _M.check_schema(conf, schema_type)
40    if schema_type == core.schema.TYPE_METADATA then
41        return core.schema.check(metadata_schema, conf)
42    end
43    return core.schema.check(schema, conf)
44end
45
46-- Log phase
47function _M.log(conf, ctx)
48    core.log.warn("conf: ", core.json.encode(conf))
49    core.log.warn("ctx: ", core.json.encode(ctx, true))
50end
51
52
53return _M

Once the minimal available plugin file is ready, the plugin's configuration data and request-related data can be output to the error.log file via core.log.warn(core.json.encode(conf)) and core.log.warn("ctx: ", core.json.encode(ctx, true)).

Enable and test the plugin

The following are a couple of steps for testing. In order to test whether the plugin can successfully print the plugin data and request-related data information we configured for it to the error log file, we need to enable the plugin and create a test route.

  1. Prepare a test upstream locally (the test upstream used in this article is 127.0.0.1:3030/api/hello, which I created locally).

  2. Create a route via curl command and enable our new plugin.

    1 curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    2 {
    3 "plugins": {
    4     "file-logger": {
    5     "path": "logs/file.log"
    6     }
    7 },
    8 "upstream": {
    9     "type": "roundrobin",
    10     "nodes": {
    11     "127.0.0.1:3030": 1
    12     }
    13 },
    14 "uri": "/api/hello"
    15 }'
    

    You will then see a status code 200, indicating that the route was successfully created.

  3. Run the curl command to send a request to the route to test whether the file-logger plugin has been started.

    1curl -i http://127.0.0.1:9080/api/hello
    2HTTP/1.1 200 OK
    3...
    4hello, world
    
  4. In the logs/error.log file there will be a record:

    record in logs/error.log

    As you can see, the path: logs/file.log that we configured for the plugin in the conf parameter has been successfully saved. At this point we have successfully created a minimally usable plugin that prints the conf and ctx parameters in the logging phase.

    After that, we can write the core functionality for the file-logger.lua plugin directly in its code file. Here we can directly run the apisix reload command to reload the latest plugin code without restarting Apache APISIX.

Write core function for the file-logger plugin

The main function of the file-logger plugin is to write log data. After asking other people from the community and checking the information, I learned about Lua's IO library, and confirmed that the logic of the plugin's function is roughly the following steps.

  1. After each accepted request, output the log data to the path configured by the plugin.

    1. First, get the value of path in file-logger through conf in the logging phase.
    2. Then, the Lua IO library is used to create, open, write, refresh the cache, and close the file.
  2. Handle errors such as open file failure, create file failure, etc.

    1 local function write_file_data(conf, log_message)
    2     local msg, err = core.json.encode(log_message)
    3     if err then
    4         return core.log.error("message json serialization failed, error info : ", err)
    5     end
    6
    7     local file, err = io_open(conf.path, 'a+')
    8
    9     if not file then
    10         core.log.error("failed to open file: ", conf.path, ", error info: ", err)
    11     else
    12         local ok, err = file:write(msg, '\n')
    13         if not ok then
    14             core.log.error("failed to write file: ", conf.path, ", error info: ", err)
    15         else
    16             file:flush()
    17         end
    18         file:close()
    19     end
    20 end
  3. Referring to the source code of http-logger plugin, I finished the method of passing the log data to the write log data and some judgment and processing of the metadata.

    1 function _M.log(conf, ctx)
    2     local metadata = plugin.plugin_metadata(plugin_name)
    3     local entry
    4
    5     if metadata and metadata.value.log_format
    6         and core.table.nkeys(metadata.value.log_format) > 0
    7     then
    8         entry = log_util.get_custom_format_log(ctx, metadata.value.log_format)
    9     else
    10         entry = log_util.get_full_log(ngx, conf)
    11     end
    12
    13     write_file_data(conf, entry)
    14 end

Validate and add tests

Validate the log records

Since the file-logger plugin was enabled when the test route was created and the path was configured as logs/file.log, we can simply send a request to the test route to verify the results of the log collection at this point.

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

In the corresponding logs/file.log we can see that each record is saved in JSON format. After formatting one of the data, it looks like this.

1{
2    "server":{
3        "hostname":"....",
4        "version":"2.11.0"
5    },
6    "client_ip":"127.0.0.1",
7    "upstream":"127.0.0.1:3030",
8    "route_id":"1",
9    "start_time":1641285122961,
10    "latency":13.999938964844,
11    "response":{
12        "status":200,
13        "size":252,
14        "headers":{
15            "server":"APISIX\/2.11.0",
16            "content-type":"application\/json; charset=utf-8",
17            "date":"Tue, 04 Jan 2022 08:32:02 GMT",
18            "vary":"Accept-Encoding",
19            "content-length":"19",
20            "connection":"close",
21            "etag":"\"13-5j0ZZR0tI549fSRsYxl8c9vAU78\""
22        }
23    },
24    "service_id":"",
25    "request":{
26        "querystring":{
27
28        },
29        "size":87,
30        "method":"GET",
31        "headers":{
32            "host":"127.0.0.1:9080",
33            "accept":"*\/*",
34            "user-agent":"curl\/7.77.0"
35        },
36        "url":"http:\/\/127.0.0.1:9080\/api\/hello",
37        "uri":"\/api\/hello"
38    }
39}

This concludes the verification of the collection of log records . The verification results indicate that the plugin was successfully launched and returned the appropriate data.

Add more tests for the plugin

For the add_block_preprocessor part of the code, I was confused when I first started writing it because I had no previous experience with Perl. After researching, I realized the correct way to use it: if we don't write request assertions and no_error_log assertions in the data section, then the default assertion is as follows.

1--- request
2GET /t
3--- no_error_log
4[error]

After taking some other logging test files into account, I created the file file-logger.t in the t/plugin/ directory.

Each test file is divided by __DATA__ into a preamble section and a data section. Since there is no clear classification of test-related documents on the official website, you can refer to the related materials at the end of the article for more details. Here is one of the test cases that I have completed after referring to the relevant materials.

1use t::APISIX 'no_plan';
2
3no_long_string();
4no_root_location();
5
6add_block_preprocessor(sub {
7    my ($block) = @_;
8
9    if (! $block->request) {
10        $block->set_value("request", "GET /t");
11    }
12
13    if (! $block->no_error_log && ! $block->error_log) {
14        $block->set_value("no_error_log", "[error]");
15    }
16});
17
18
19run_tests;
20
21__DATA__
22
23=== TEST 1: sanity
24--- config
25    location /t {
26        content_by_lua_block {
27            local configs = {
28                -- full configuration
29                {
30                    path = "file.log"
31                },
32                -- property "path" is required
33                {
34                    path = nil
35                }
36            }
37
38            local plugin = require("apisix.plugins.file-logger")
39
40            for i = 1, #configs do
41                ok, err = plugin.check_schema(configs[i])
42                if err then
43                    ngx.say(err)
44                else
45                    ngx.say("done")
46                end
47            end
48        }
49    }
50--- response_body_like
51done
52property "path" is required

This concludes the plugin addition test session.

Summary

The above is the whole process of implementing an Apache APISIX plugin from 0 as a newbie in the backend. I did encounter a lot of pitfalls in the process of developing the plugin, but luckily there are many enthusiastic brothers in the Apache APISIX community to help me solve the problems, which made the development and testing of the file-logger plugin relatively smooth throughout. If you are interested in this plugin, or want to see the details of the plugin, you can refer to the official Apache APISIX documentation.

Apache APISIX is also currently working on other plugins to support more integration services, so if you're interested, feel free to start a discussion in the GitHub Discussion, or via the mailing list.

References