¿Cómo Construir un Plugin de Apache APISIX Desde 0 Hasta 1?

Qi Guo

Qi Guo

February 16, 2022

Ecosystem

Durante los últimos meses, los usuarios de la comunidad han añadido muchos complementos a Apache APISIX, enriqueciendo el ecosistema de Apache APISIX. Desde el punto de vista del usuario, la aparición de complementos más diversos es sin duda algo positivo, ya que cumplen con más expectativas del usuario para una puerta de enlace que sea un procesador "todo en uno" y "multifuncional", además de perfeccionar el alto rendimiento y la baja latencia de Apache APISIX.

Ninguno de los artículos en el blog de Apache APISIX parece profundizar en el proceso de desarrollo de complementos. Así que echemos un vistazo al proceso desde la perspectiva de un desarrollador de complementos y veamos cómo nace un complemento.

Este artículo documenta el proceso de desarrollo del complemento file-logger por un ingeniero front-end sin experiencia en back-end. Antes de profundizar en los detalles del proceso de implementación, presentaremos brevemente la funcionalidad de file-logger.

Introducción al complemento file-logger

file-logger permite generar formatos de registro personalizados utilizando los metadatos de los complementos de Apache APISIX. Los usuarios pueden añadir datos de solicitud y respuesta en formato JSON a archivos de registro mediante el complemento file-logger, o enviar el flujo de datos de registro a una ubicación específica.

Imagina esto: al monitorear el registro de acceso de una ruta, no solo nos importa el valor de ciertos datos de solicitud y respuesta, sino que también queremos escribir los datos de registro en un archivo específico. Aquí es donde el complemento file-logger puede ayudar a lograr estos objetivos.

how it works

Podemos usar file-logger para escribir datos de registro en un archivo de registro específico y simplificar el proceso de monitoreo y depuración.

¿Cómo implementar un complemento?

Después de introducir las características de file-logger, tendrás una mejor comprensión de este complemento. A continuación, se explica en detalle cómo yo, un desarrollador front-end sin experiencia en el lado del servidor, desarrollo el complemento para Apache APISIX y añado las pruebas correspondientes.

Confirmar el nombre y la prioridad del complemento

Abre la Guía de desarrollo de complementos de Apache APISIX y, en orden de prioridad, debes determinar lo siguiente:

  1. Determinar la categoría del complemento.
  2. Priorizar los complementos y actualizar el archivo conf/config-default.yaml.

Dado que este desarrollo de file-logger es un complemento de tipo registro, me refiero al nombre y orden de los complementos de registro existentes para Apache APISIX y coloco file-logger aquí.

file-logger's position

Después de consultar con otros autores de complementos y miembros entusiastas de la comunidad, finalmente se confirmó el nombre file-logger y la prioridad 399 del complemento.

Nota: La prioridad del complemento está relacionada con el orden de ejecución; cuanto mayor sea el valor de la prioridad, más adelante se ejecutará. Y el orden de los nombres de los complementos no está relacionado con el orden de ejecución.

Crear un archivo de complemento mínimamente ejecutable

Después de confirmar el nombre y la prioridad del complemento, puedes crear nuestro archivo de código del complemento en el directorio apisix/plugins/. Hay dos puntos a tener en cuenta aquí:

  • Si el archivo de código del complemento se crea directamente en el directorio apisix/plugins/, no es necesario cambiar el archivo Makefile.
  • Si tu complemento tiene su propio directorio de código, debes actualizar el archivo Makefile. Consulta la Guía de desarrollo de complementos de Apache APISIX para obtener los pasos detallados.
  1. Aquí creamos el archivo file-logger.lua en el directorio apisix/plugins/.
  2. Luego completaremos una versión inicial basada en el example-plugin.
-- Introducir el módulo que necesitamos en la cabecera
local log_util     =   require("apisix.utils.log-util")
local core         =   require("apisix.core")
local plugin       =   require("apisix.plugin")
local ngx          =   ngx

-- Declarar el nombre del complemento
local plugin_name = "file-logger"

-- Definir el formato del esquema del complemento
local schema = {
    type = "object",
    properties = {
        path = {
            type = "string"
        },
    },
    required = {"path"}
}

-- Esquema de metadatos del complemento
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 si la configuración del complemento es correcta
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 registro
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

Una vez que el archivo de complemento mínimamente ejecutable está listo, los datos de configuración del complemento y los datos relacionados con la solicitud se pueden enviar al archivo error.log mediante core.log.warn(core.json.encode(conf)) y core.log.warn("ctx: ", core.json.encode(ctx, true)).

Habilitar y probar el complemento

A continuación, se presentan algunos pasos para realizar pruebas. Para probar si el complemento puede imprimir correctamente los datos del complemento y la información relacionada con la solicitud que configuramos en el archivo de registro de errores, necesitamos habilitar el complemento y crear una ruta de prueba.

  1. Prepara un upstream de prueba localmente (el upstream de prueba utilizado en este artículo es 127.0.0.1:3030/api/hello, que creé localmente).

  2. Crea una ruta mediante el comando curl y habilita nuestro nuevo complemento.

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

    Luego verás un código de estado 200, lo que indica que la ruta se creó correctamente.

  3. Ejecuta el comando curl para enviar una solicitud a la ruta y probar si el complemento file-logger se ha iniciado.

    curl -i http://127.0.0.1:9080/api/hello
    HTTP/1.1 200 OK
    ...
    hello, world
    
  4. En el archivo logs/error.log habrá un registro:

    record in logs/error.log

    Como puedes ver, el path: logs/file.log que configuramos para el complemento en el parámetro conf se ha guardado correctamente. En este punto, hemos creado con éxito un complemento mínimamente utilizable que imprime los parámetros conf y ctx en la fase de registro.

    Después de eso, podemos escribir la funcionalidad principal para el complemento file-logger.lua directamente en su archivo de código. Aquí podemos ejecutar directamente el comando apisix reload para recargar el código más reciente del complemento sin reiniciar Apache APISIX.

Escribir la función principal para el complemento file-logger

La función principal del complemento file-logger es escribir datos de registro. Después de consultar con otras personas de la comunidad y revisar la documentación, aprendí sobre la biblioteca IO de Lua y confirmé que la lógica de la función del complemento sigue aproximadamente los siguientes pasos.

  1. Después de cada solicitud aceptada, enviar los datos de registro al path configurado por el complemento.

    1. Primero, obtener el valor de path en file-logger a través de conf en la fase de registro.
    2. Luego, usar la biblioteca IO de Lua para crear, abrir, escribir, refrescar la caché y cerrar el archivo.
  2. Manejar errores como fallos al abrir el archivo, fallos al crear el archivo, 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. Refiriéndome al código fuente del complemento http-logger, completé el método de pasar los datos de registro a la función de escritura de datos de registro y algunos juicios y procesamientos de los metadatos.

    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 y añadir pruebas

Validar los registros

Dado que el complemento file-logger se habilitó cuando se creó la ruta de prueba y se configuró el path como logs/file.log, podemos simplemente enviar una solicitud a la ruta de prueba para verificar los resultados de la recopilación de registros en este punto.

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

En el archivo logs/file.log correspondiente, podemos ver que cada registro se guarda en formato JSON. Después de formatear uno de los datos, se ve así.

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

Esto concluye la verificación de la recopilación de registros. Los resultados de la verificación indican que el complemento se lanzó con éxito y devolvió los datos apropiados.

Añadir más pruebas para el complemento

Para la parte del código add_block_preprocessor, estaba confundido cuando comencé a escribirlo porque no tenía experiencia previa con Perl. Después de investigar, me di cuenta de la forma correcta de usarlo: si no escribimos aserciones de request y no_error_log en la sección de datos, entonces la aserción predeterminada es la siguiente.

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

Después de revisar algunos otros archivos de prueba de registro, creé el archivo file-logger.t en el directorio t/plugin/.

Cada archivo de prueba está dividido por **DATA** en una sección de preámbulo y una sección de datos. Dado que no hay una clasificación clara de los documentos relacionados con pruebas en el sitio web oficial, puedes consultar los materiales relacionados al final del artículo para obtener más detalles. Aquí tienes uno de los casos de prueba que completé después de consultar los materiales relacionados.

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

Esto concluye la sesión de pruebas de adición de complementos.

Resumen

Lo anterior es todo el proceso de implementación de un complemento de Apache APISIX desde cero como un principiante en el backend. En el proceso de desarrollo del complemento, me encontré con muchos obstáculos, pero afortunadamente hay muchos compañeros entusiastas en la comunidad de Apache APISIX que me ayudaron a resolver los problemas, lo que hizo que el desarrollo y las pruebas del complemento file-logger fueran relativamente fluidos en general. Si estás interesado en este complemento o quieres ver los detalles del complemento, puedes consultar la documentación oficial de Apache APISIX.

Apache APISIX también está trabajando actualmente en otros complementos para admitir más servicios de integración, por lo que si estás interesado, no dudes en iniciar una discusión en GitHub Discussion o a través de la lista de correo.

Referencias

Tags: