Comment construire un plugin Apache APISIX de 0 à 1 ?

Qi Guo

Qi Guo

February 16, 2022

Ecosystem

Au cours des derniers mois, les utilisateurs de la communauté ont ajouté de nombreux plugins à Apache APISIX, enrichissant ainsi l'écosystème d'Apache APISIX. Du point de vue de l'utilisateur, l'émergence de plugins plus diversifiés est certainement une bonne chose, car ils répondent davantage aux attentes des utilisateurs pour une passerelle qui soit un processeur "tout-en-un" et "multifonction", tout en perfectionnant les performances élevées et la faible latence d'Apache APISIX.

Aucun des articles sur le blog d'Apache APISIX ne semble détailler le processus de développement des plugins. Alors, examinons ce processus du point de vue d'un développeur de plugin et voyons comment un plugin naît !

Cet article documente le processus de développement du plugin file-logger par un ingénieur front-end sans expérience en back-end. Avant de plonger dans les détails du processus de mise en œuvre, nous présenterons brièvement la fonctionnalité de file-logger.

Introduction du plugin file-logger

file-logger prend en charge la génération de formats de journaux personnalisés en utilisant les métadonnées des plugins d'Apache APISIX. Les utilisateurs peuvent ajouter des données de requête et de réponse au format JSON dans des fichiers de journaux via le plugin file-logger, ou pousser le flux de données de journaux vers un emplacement spécifié.

Imaginez ceci : lors de la surveillance du journal d'accès d'une route, nous ne nous intéressons pas seulement à la valeur de certaines données de requête et de réponse, mais nous souhaitons également écrire les données de journal dans un fichier spécifié. C'est là que le plugin file-logger peut être utilisé pour aider à atteindre ces objectifs.

how it works

Nous pouvons utiliser file-logger pour écrire les données de journal dans un fichier de journal spécifique afin de simplifier le processus de surveillance et de débogage.

Comment implémenter un plugin ?

Après avoir introduit les fonctionnalités de file-logger, vous aurez une meilleure compréhension de ce plugin. Voici une explication détaillée de la manière dont moi, en tant que développeur front-end sans expérience côté serveur, j'ai développé le plugin pour Apache APISIX et ajouté les tests correspondants.

Confirmer le nom et la priorité du plugin

Ouvrez le Guide de développement de plugins Apache APISIX et, par ordre de priorité, vous devez déterminer les deux choses suivantes :

  1. Déterminer la catégorie du plugin.
  2. Prioriser les plugins et mettre à jour le fichier conf/config-default.yaml.

Comme ce développement de file-logger est un plugin de type journalisation, je me réfère au nom et à l'ordre des plugins de journalisation existants pour Apache APISIX et place file-logger ici.

file-logger's position

Après avoir consulté d'autres auteurs de plugins et des membres enthousiastes de la communauté, le nom file-logger et la priorité 399 du plugin ont finalement été confirmés.

Notez que la priorité du plugin est liée à l'ordre d'exécution ; plus la valeur de la priorité est élevée, plus l'exécution est avancée. Et l'ordre des noms de plugins n'est pas lié à l'ordre d'exécution.

Créer un fichier de plugin minimal exécutable

Après avoir confirmé le nom et la priorité du plugin, vous pouvez créer notre fichier de code de plugin dans le répertoire apisix/plugins/. Il y a deux points à noter ici :

  • Si le fichier de code du plugin est créé directement dans le répertoire apisix/plugins/, il n'est pas nécessaire de modifier le fichier Makefile.
  • Si votre plugin a son propre répertoire de code, vous devez mettre à jour le fichier Makefile, veuillez vous référer au Guide de développement de plugins Apache APISIX pour les étapes détaillées.
  1. Ici, nous créons le fichier file-logger.lua dans le répertoire apisix/plugins/.
  2. Ensuite, nous compléterons une version initialisée basée sur le example-plugin.
-- Introduire le module dont nous avons besoin dans l'en-tête
local log_util     =   require("apisix.utils.log-util")
local core         =   require("apisix.core")
local plugin       =   require("apisix.plugin")
local ngx          =   ngx

-- Déclarer le nom du plugin
local plugin_name = "file-logger"

-- Définir le format du schéma du plugin
local schema = {
    type = "object",
    properties = {
        path = {
            type = "string"
        },
    },
    required = {"path"}
}

-- Schéma des métadonnées du 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
}

-- Vérifier si la configuration du plugin est correcte
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

-- Phase de journalisation
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

Une fois le fichier de plugin minimal exécutable prêt, les données de configuration du plugin et les données liées à la requête peuvent être sorties dans le fichier error.log via core.log.warn(core.json.encode(conf)) et core.log.warn("ctx: ", core.json.encode(ctx, true)).

Activer et tester le plugin

Voici quelques étapes pour les tests. Afin de tester si le plugin peut imprimer avec succès les données du plugin et les informations de données liées à la requête que nous avons configurées pour lui dans le fichier de journal d'erreurs, nous devons activer le plugin et créer une route de test.

  1. Préparez un amont de test localement (l'amont de test utilisé dans cet article est 127.0.0.1:3030/api/hello, que j'ai créé localement).

  2. Créez une route via la commande curl et activez notre nouveau 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"
    }'
    

    Vous verrez ensuite un code d'état 200, indiquant que la route a été créée avec succès.

  3. Exécutez la commande curl pour envoyer une requête à la route afin de tester si le plugin file-logger a été démarré.

    curl -i http://127.0.0.1:9080/api/hello
    HTTP/1.1 200 OK
    ...
    hello, world
    
  4. Dans le fichier logs/error.log, il y aura un enregistrement :

    record in logs/error.log

    Comme vous pouvez le voir, le path: logs/file.log que nous avons configuré pour le plugin dans le paramètre conf a été enregistré avec succès. À ce stade, nous avons créé avec succès un plugin minimal exécutable qui imprime les paramètres conf et ctx dans la phase de journalisation.

    Ensuite, nous pouvons écrire la fonctionnalité principale pour le plugin file-logger.lua directement dans son fichier de code. Ici, nous pouvons directement exécuter la commande apisix reload pour recharger le dernier code du plugin sans redémarrer Apache APISIX.

Écrire la fonction principale pour le plugin file-logger

La fonction principale du plugin file-logger est d'écrire les données de journal. Après avoir demandé à d'autres membres de la communauté et vérifié les informations, j'ai appris à connaître la bibliothèque IO de Lua et confirmé que la logique de la fonction du plugin est à peu près les étapes suivantes.

  1. Après chaque requête acceptée, sortir les données de journal dans le path configuré par le plugin.

    1. Tout d'abord, obtenir la valeur de path dans file-logger via conf dans la phase de journalisation.
    2. Ensuite, utiliser la bibliothèque IO de Lua pour créer, ouvrir, écrire, rafraîchir le cache et fermer le fichier.
  2. Gérer les erreurs telles que l'échec de l'ouverture du fichier, l'échec de la création du fichier, 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. En me référant au code source du plugin http-logger, j'ai terminé la méthode de transmission des données de journal à l'écriture des données de journal et quelques jugements et traitements des métadonnées.

    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
    

Valider et ajouter des tests

Valider les enregistrements de journal

Comme le plugin file-logger a été activé lors de la création de la route de test et que le chemin a été configuré comme logs/file.log, nous pouvons simplement envoyer une requête à la route de test pour vérifier les résultats de la collecte de journaux à ce stade.

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

Dans le fichier logs/file.log correspondant, nous pouvons voir que chaque enregistrement est sauvegardé au format JSON. Après avoir formaté l'une des données, cela ressemble à ceci.

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

Cela conclut la vérification de la collecte des enregistrements de journal. Les résultats de la vérification indiquent que le plugin a été lancé avec succès et a renvoyé les données appropriées.

Ajouter plus de tests pour le plugin

Pour la partie add_block_preprocessor du code, j'étais confus lorsque j'ai commencé à l'écrire car je n'avais aucune expérience préalable en Perl. Après avoir fait des recherches, j'ai compris la bonne façon de l'utiliser : si nous n'écrivons pas d'assertions request et no_error_log dans la section des données, alors l'assertion par défaut est la suivante.

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

Après avoir pris en compte d'autres fichiers de test de journalisation, j'ai créé le fichier file-logger.t dans le répertoire t/plugin/.

Chaque fichier de test est divisé par **DATA** en une section de préambule et une section de données. Comme il n'y a pas de classification claire des documents liés aux tests sur le site officiel, vous pouvez vous référer aux documents connexes à la fin de l'article pour plus de détails. Voici l'un des cas de test que j'ai complété après avoir consulté les documents pertinents.

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

Cela conclut la session d'ajout de tests pour le plugin.

Résumé

Ce qui précède est l'ensemble du processus de mise en œuvre d'un plugin Apache APISIX à partir de 0 en tant que débutant en back-end. J'ai rencontré beaucoup de pièges dans le processus de développement du plugin, mais heureusement, il y a de nombreux membres enthousiastes dans la communauté Apache APISIX pour m'aider à résoudre les problèmes, ce qui a rendu le développement et les tests du plugin file-logger relativement fluides tout au long. Si vous êtes intéressé par ce plugin, ou si vous souhaitez voir les détails du plugin, vous pouvez vous référer à la documentation officielle d'Apache APISIX.

Apache APISIX travaille également actuellement sur d'autres plugins pour prendre en charge davantage de services d'intégration, donc si vous êtes intéressé, n'hésitez pas à démarrer une discussion dans les Discussions GitHub, ou via la liste de diffusion.

Références

Tags: