Criar um plugin (tutorial)

Este é um tutorial passo a passo sobre como criar um plugin simples carregável para o Zabbix agent 2.

Você também pode usar o repositório de exemplo como um modelo ou guia para criar seus próprios plugins.

O que você irá criar

Este tutorial demonstra como criar um novo plugin carregável MyIP. O plugin implementará uma única chave de item, myip, que retorna o endereço IP externo do host onde o Zabbix agent 2 está em execução.

Passo 1: Configuração

  1. Um plugin é um módulo Go padrão. Comece inicializando o arquivo go.mod no diretório do plugin para rastrear as dependências do plugin:
cd path/to/plugins/myip # Mude para o diretório do seu plugin
go mod init myip
  1. Instale a dependência obrigatória Zabbix Go SDK (golang.zabbix.com/sdk):
go get golang.zabbix.com/sdk@$LATEST_COMMIT_HASH

Substitua $LATEST_COMMIT_HASH pelo hash do commit HEAD mais recente do repositório golang.zabbix.com/sdk repository no branch de release apropriado. Por exemplo:

go get golang.zabbix.com/sdk@af85407

Observe que a versão do golang.zabbix.com/sdk atualmente não é suportada, mas isso pode mudar no futuro.

Dependências adicionais podem ser instaladas conforme necessário usando go get.

  1. Crie um arquivo main.go vazio para o código-fonte do plugin:
touch main.go

Agora a configuração inicial está concluída e o plugin está pronto para desenvolvimento.

Etapa 2: Estrutura do plugin

O módulo golang.zabbix.com/sdk, instalado na etapa anterior, fornece a estrutura necessária para o desenvolvimento de plugins e garante que todos os plugins tenham uma estrutura consistente.

  1. Configure o fluxo básico de execução.

Comece definindo o fluxo principal de execução do plugin. Adicione o seguinte código ao main.go:

package main

func main() {
    err := run()
    if err != nil {
        panic(err)
    }
}

func run() error {
    return nil
}

Isso estabelece o fluxo básico de execução para o plugin. A função run conterá posteriormente a lógica principal do plugin.

  1. Explore as interfaces do plugin.

Um plugin do Zabbix agent 2 deve ser representado por uma struct que implementa interfaces do pacote golang.zabbix.com/sdk/plugin:

  • Accessor - define métodos essenciais que todos os plugins devem implementar, como definir o nome do plugin e lidar com timeouts de item key.
  • Uma ou mais das seguintes interfaces funcionais de plugin:
    • Exporter - executa uma pesquisa e retorna um valor (ou valores), nada ou um erro; frequentemente usado junto com a interface Collector.
    • Collector - gerencia a coleta periódica de dados.
    • Runner - define procedimentos de inicialização e desligamento do plugin.
    • Watcher - permite implementar polling de métricas de forma independente, ignorando o agendador interno do agent; útil para monitoramento baseado em trap ou orientado a eventos.
    • Configurator - define como o plugin lê e aplica suas configurações.

Você pode implementar essas interfaces por conta própria ou usar as implementações padrão fornecidas pelo Zabbix Go SDK, modificando-as conforme necessário. Este tutorial usa as implementações padrão.

  1. Crie a struct do plugin.

Agora, importe o pacote plugin e crie uma struct myIP que incorpora a struct plugin.Base:

import "golang.zabbix.com/sdk/plugin"

type myIP struct {
    plugin.Base
}

A struct myIP atualmente satisfaz a interface Accessor. Um método para implementar uma das interfaces funcionais do plugin, o Exporter, será adicionado posteriormente no tutorial.

Etapa 3: Definir as chaves dos items

Seu plugin precisa das chaves dos items para coletar dados e fornecê-los ao Zabbix server ou proxy.

  1. Importe o pacote errs para tratamento de erros:
import "golang.zabbix.com/sdk/errs"
  1. Registre as chaves dos items usando a função plugin.RegisterMetrics() dentro da função run():
func run() error {
    p := &myIP{}

    // Registra a chave do item `myip`.
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // Nome do plugin
        "myip",                           // Nome da chave do item
        "Returns the host's IP address.", // Descrição da chave do item
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    return nil
}

Para registrar várias chaves de items, repita os parâmetros nome da métrica e descrição para cada métrica. Por exemplo:

plugin.RegisterMetrics(&impl, "Myip", "metric.one", "Metric one description.", "metric.two", "Metric two description.")

Etapa 4: Configurar o handler

O handler facilita a comunicação entre o agent e o plugin.

  1. Importe o pacote container:
import "golang.zabbix.com/sdk/plugin/container"
  1. Dentro da função run(), adicione o código para criar e configurar um handler:
func run() error {
    p := &myIP{}

    // Registra a chave de item `myip`.
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // Nome do plugin
        "myip",                           // Nome da chave do item
        "Returns the host's IP address.", // Descrição da chave do item
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    // Cria um novo handler.
    h, err := container.NewHandler("MyIP") // Nome do plugin
    if err != nil {
        return errs.Wrap(err, "failed to create new handler")
    }

    // Configura o log para encaminhar logs do plugin para o agent.
    // Disponível via p.Logger.Infof, p.Logger.Debugf, etc.
    p.Logger = h

    // Inicia a execução do plugin.
    // Bloqueia até que uma solicitação de término seja recebida do agent.
    err = h.Execute()
    if err != nil {
        return errs.Wrap(err, "failed to execute plugin handler")
    }

    return nil
}

Etapa 5: Implementar a coleta de dados

A coleta de dados é feita por meio da interface Exporter, que descreve o método Export:

func Export(
  key string,             // A chave do item a ser coletada.
  params []string,        // Argumentos passados para a chave do item (`myip[arg1, arg2]`).
  context ContextProvider // Metadados sobre a coleta de dados da chave do item.
) (any, error)
  1. Importe os pacotes necessários para requisições HTTP e leitura de respostas:
import (
    "io"
    "net/http"
)
  1. Implemente o método Export para a struct myIP:
func (p *myIP) Export(
    key string, params []string, context plugin.ContextProvider,
) (any, error) {
    // O plugin pode usar diferentes lógicas de coleta de dados com base no parâmetro `key`.
    // Esta implementação apenas verifica se a `key` fornecida é suportada.
    if key != "myip" {
        return nil, errs.Errorf("unknown item key %q", key)
    }

    // O log será encaminhado para o log do agent 2.
    p.Infof(
        "received request to handle %q key with %d parameters",
        key,
        len(params),
    )

    // Colete os dados e retorne-os.

    resp, err := http.Get("https://api.ipify.org")
    if err != nil {
        return nil, errs.Wrap(err, "failed to get IP address")
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, errs.Wrap(err, "failed to read response body")
    }

    return string(body), nil
}

Etapa 6: Compile e configure o plugin

  1. Para compilar o plugin, execute:
go mod tidy
go build

Isso deve criar um executável myip no diretório atual.

  1. Configure o Zabbix agent 2 para usar o plugin:
echo "Plugins.MyIP.System.Path=$PATH_TO_THE_MYIP_PLUGIN_EXECUTABLE" > /etc/zabbix_agent2.d/plugins.d/myip.conf

Substitua $PATH_TO_THE_MYIP_PLUGIN_EXECUTABLE pelo caminho para o myip criado na etapa 5.

O nome do plugin no parâmetro de configuração (MyIP neste tutorial) deve corresponder ao nome do plugin definido na função plugin.RegisterMetrics().

  1. Para testar o plugin e seu item myip, execute:
zabbix_agent2 -c /etc/zabbix_agent2.conf -t myip

A saída deve conter um endereço IP externo do seu host e ser semelhante a esta:

myip                                          [s|192.0.2.0]

Com isso, você criou um plugin simples carregável para o Zabbix agent 2. Parabéns!

Código-fonte completo

package main

import (
    "io"
    "net/http"

    "golang.zabbix.com/sdk/errs"
    "golang.zabbix.com/sdk/plugin"
    "golang.zabbix.com/sdk/plugin/container"
)

var _ plugin.Exporter = (*myIP)(nil)

type myIP struct {
    plugin.Base
}

func main() {
    err := run()
    if err != nil {
        panic(err)
    }
}

func run() error {
    p := &myIP{}

    // Registra a chave de item `myip`.
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // Nome do plugin
        "myip",                           // Nome da chave do item
        "Returns the host's IP address.", // Descrição da chave do item
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    // Cria um novo handler.
    h, err := container.NewHandler("MyIP") // Nome do plugin
    if err != nil {
        return errs.Wrap(err, "failed to create new handler")
    }

    // Configura o log para encaminhar logs do plugin para o agent.
    // Disponível via p.Logger.Infof, p.Logger.Debugf, etc.
    p.Logger = h

    // Inicia a execução do plugin.
    // Bloqueia até que uma solicitação de término do agent seja recebida.
    err = h.Execute()
    if err != nil {
        return errs.Wrap(err, "failed to execute plugin handler")
    }

    return nil
}

func (p *myIP) Export(
    key string, params []string, context plugin.ContextProvider,
) (any, error) {
    // O plugin pode usar diferentes lógicas de coleta de dados com base no parâmetro `key`.
    // Esta implementação apenas verifica se a `key` fornecida é suportada.
    if key != "myip" {
        return nil, errs.Errorf("unknown item key %q", key)
    }

    // O log será encaminhado para o log do agent 2.
    p.Infof(
        "received request to handle %q key with %d parameters",
        key,
        len(params),
    )

    // Coleta os dados e os retorna.

    resp, err := http.Get("https://api.ipify.org")
    if err != nil {
        return nil, errs.Wrap(err, "failed to get IP address")
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, errs.Wrap(err, "failed to read response body")
    }

    return string(body), nil
}