Como Construir um Plugin do Apache APISIX do Zero ao Um?

Qi Guo

Qi Guo

February 16, 2022

Ecosystem

Nos últimos meses, os usuários da comunidade adicionaram muitos plugins ao Apache APISIX, enriquecendo o ecossistema do Apache APISIX. Do ponto de vista do usuário, o surgimento de plugins mais diversos é certamente uma coisa boa, pois atendem a mais expectativas dos usuários em relação a um gateway que seja um processador "tudo-em-um" e "multifuncional", além de aprimorar o alto desempenho e a baixa latência do Apache APISIX.

Nenhum dos artigos no blog do Apache APISIX parece detalhar o processo de desenvolvimento de plugins. Então, vamos dar uma olhada no processo do ponto de vista de um desenvolvedor de plugins e ver como um plugin nasce!

Este artigo documenta o processo de desenvolvimento do plugin file-logger por um engenheiro de front-end sem experiência em back-end. Antes de mergulhar nos detalhes do processo de implementação, vamos apresentar brevemente a funcionalidade do file-logger.

Introdução ao plugin file-logger

O file-logger suporta a geração de formatos de log personalizados usando metadados de plugins do Apache APISIX. Os usuários podem anexar dados de solicitação e resposta em formato JSON a arquivos de log por meio do plugin file-logger, ou enviar o fluxo de dados de log para um local especificado.

Imagine o seguinte: ao monitorar o log de acesso de uma rota, não nos importamos apenas com o valor de certos dados de solicitação e resposta, mas também queremos escrever os dados de log em um arquivo especificado. É aqui que o plugin file-logger pode ser usado para ajudar a alcançar esses objetivos.

como funciona

Podemos usar o file-logger para escrever dados de log em um arquivo de log específico, simplificando o processo de monitoramento e depuração.

Como implementar um plugin?

Após a introdução das funcionalidades do file-logger, você terá uma melhor compreensão deste plugin. A seguir, está uma explicação detalhada de como eu, um desenvolvedor de front-end sem experiência em servidor, desenvolvi o plugin para o Apache APISIX e adicionei os testes correspondentes.

Confirmar o nome e a prioridade do plugin

Abra o Guia de Desenvolvimento de Plugins do Apache APISIX e, em ordem de prioridade, você precisa determinar as seguintes duas coisas:

  1. Determinar a categoria do plugin.
  2. Priorizar os plugins e atualizar o arquivo conf/config-default.yaml.

Como este desenvolvimento do file-logger é um plugin do tipo log, eu me referi ao nome e à ordenação dos plugins de log existentes do Apache APISIX e coloquei o file-logger aqui.

posição do file-logger

Após consultar outros autores de plugins e membros entusiastas da comunidade, o nome file-logger e a prioridade 399 do plugin foram finalmente confirmados.

Observe que a prioridade do plugin está relacionada à ordem de execução; quanto maior o valor da prioridade, mais à frente será a execução. E a ordenação dos nomes dos plugins não está relacionada à ordem de execução.

Criar um arquivo de plugin mínimo executável

Após confirmar o nome e a prioridade do plugin, você pode criar nosso arquivo de código do plugin no diretório apisix/plugins/. Há dois pontos a serem observados aqui:

  • Se o arquivo de código do plugin for criado diretamente no diretório apisix/plugins/, não há necessidade de alterar o arquivo Makefile.
  • Se o seu plugin tiver seu próprio diretório de código, você precisará atualizar o arquivo Makefile. Consulte o Guia de Desenvolvimento de Plugins do Apache APISIX para etapas detalhadas.
  1. Aqui criamos o arquivo file-logger.lua no diretório apisix/plugins/.
  2. Em seguida, completaremos uma versão inicializada com base no example-plugin.
-- Introduzir o módulo que precisamos no cabeçalho
local log_util     =   require("apisix.utils.log-util")
local core         =   require("apisix.core")
local plugin       =   require("apisix.plugin")
local ngx          =   ngx

-- Declarar o nome do plugin
local plugin_name = "file-logger"

-- Definir o formato do esquema do plugin
local schema = {
    type = "object",
    properties = {
        path = {
            type = "string"
        },
    },
    required = {"path"}
}

-- Esquema de metadados do plugin
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
}

-- Verificar se a configuração do plugin está correta
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

-- Fase de log
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

Uma vez que o arquivo de plugin mínimo disponível esteja pronto, os dados de configuração do plugin e os dados relacionados à solicitação podem ser enviados para o arquivo error.log por meio de core.log.warn(core.json.encode(conf)) e core.log.warn("ctx: ", core.json.encode(ctx, true)).

Habilitar e testar o plugin

A seguir estão algumas etapas para teste. Para testar se o plugin pode imprimir com sucesso os dados do plugin e as informações de dados relacionados à solicitação que configuramos para ele no arquivo de log de erro, precisamos habilitar o plugin e criar uma rota de teste.

  1. Prepare um upstream de teste localmente (o upstream de teste usado neste artigo é 127.0.0.1:3030/api/hello, que criei localmente).

  2. Crie uma rota por meio do comando curl e habilite nosso novo plugin.

    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"
    }'
    

    Você verá então um código de status 200, indicando que a rota foi criada com sucesso.

  3. Execute o comando curl para enviar uma solicitação à rota para testar se o plugin file-logger foi iniciado.

    curl -i http://127.0.0.1:9080/api/hello
    HTTP/1.1 200 OK
    ...
    hello, world
    
  4. No arquivo logs/error.log haverá um registro:

    registro em logs/error.log

    Como você pode ver, o path: logs/file.log que configuramos para o plugin no parâmetro conf foi salvo com sucesso. Neste ponto, criamos com sucesso um plugin mínimo utilizável que imprime os parâmetros conf e ctx na fase de log.

    Depois disso, podemos escrever a funcionalidade principal para o plugin file-logger.lua diretamente em seu arquivo de código. Aqui, podemos executar diretamente o comando apisix reload para recarregar o código mais recente do plugin sem reiniciar o Apache APISIX.

Escrever a função principal para o plugin file-logger

A função principal do plugin file-logger é escrever dados de log. Após perguntar a outras pessoas da comunidade e verificar as informações, aprendi sobre a biblioteca IO do Lua e confirmei que a lógica da função do plugin é aproximadamente as seguintes etapas.

  1. Após cada solicitação aceita, enviar os dados de log para o path configurado pelo plugin.

    1. Primeiro, obtenha o valor de path no file-logger por meio de conf na fase de log.
    2. Em seguida, a biblioteca IO do Lua é usada para criar, abrir, escrever, atualizar o cache e fechar o arquivo.
  2. Lidar com erros como falha ao abrir o arquivo, falha ao criar o arquivo, etc.

    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. Referenciando o código-fonte do plugin http-logger, concluí o método de passar os dados de log para os dados de log de escrita e alguns julgamentos e processamentos dos metadados.

    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
    

Validar e adicionar testes

Validar os registros de log

Como o plugin file-logger foi habilitado quando a rota de teste foi criada e o caminho foi configurado como logs/file.log, podemos simplesmente enviar uma solicitação para a rota de teste para verificar os resultados da coleta de log neste momento.

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

No arquivo correspondente logs/file.log, podemos ver que cada registro é salvo em formato JSON. Após formatar um dos dados, ele se parece com isso.

{
  "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"
  }
}

Isso conclui a verificação da coleta de registros de log. Os resultados da verificação indicam que o plugin foi iniciado com sucesso e retornou os dados apropriados.

Adicionar mais testes para o plugin

Para a parte do código add_block_preprocessor, eu estava confuso quando comecei a escrevê-lo porque não tinha experiência anterior com Perl. Após pesquisar, percebi a maneira correta de usá-lo: se não escrevermos asserções request e no_error_log na seção de dados, então a asserção padrão é a seguinte.

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

Após considerar alguns outros arquivos de teste de log, criei o arquivo file-logger.t no diretório t/plugin/.

Cada arquivo de teste é dividido por **DATA** em uma seção de preâmbulo e uma seção de dados. Como não há uma classificação clara de documentos relacionados a testes no site oficial, você pode consultar os materiais relacionados no final do artigo para mais detalhes. Aqui está um dos casos de teste que concluí após consultar os materiais relevantes.

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

Isso conclui a sessão de teste de adição de plugin.

Resumo

O acima é todo o processo de implementação de um plugin do Apache APISIX do zero como um iniciante no back-end. Eu realmente encontrei muitos obstáculos no processo de desenvolvimento do plugin, mas felizmente há muitos irmãos entusiastas na comunidade do Apache APISIX para me ajudar a resolver os problemas, o que tornou o desenvolvimento e teste do plugin file-logger relativamente tranquilo. Se você estiver interessado neste plugin, ou quiser ver os detalhes do plugin, você pode consultar a documentação oficial do Apache APISIX.

O Apache APISIX também está trabalhando atualmente em outros plugins para suportar mais serviços de integração, então, se você estiver interessado, sinta-se à vontade para iniciar uma discussão no GitHub Discussion, ou via lista de e-mails.

Referências

Tags: