Создание плагина (руководство)

Это пошаговое руководство по созданию простого загружаемого плагина для Zabbix Agent 2.

Вы также можете использовать репозиторий примеров в качестве шаблона или руководства для создания собственных плагинов.

Что вы создадите

В этом руководстве показано, как создать новый загружаемый плагин MyIP. Плагин будет реализовывать один ключ элемента, myip, который возвращает внешний IP-адрес хоста, на котором запущен Zabbix агент 2.

Шаг 1: Настройка

  1. Плагин — это стандартный модуль Go.

Начните с инициализации файла go.mod в каталоге плагина для отслеживания зависимостей плагина:

cd path/to/plugins/myip # Перейдите в каталог вашего плагина
go mod init myip
  1. Установите обязательную зависимость Zabbix Go SDK (golang.zabbix.com/sdk):
go get golang.zabbix.com/sdk@$LATEST_COMMIT_HASH

Замените $LATEST_COMMIT_HASH на последний хеш коммита HEAD из golang.zabbix.com/sdk репозитория в соответствующей ветке релиза. Например:

go get golang.zabbix.com/sdk@af85407

Обратите внимание, что версионирование golang.zabbix.com/sdk в настоящее время не поддерживается, но это может измениться в будущем.

При необходимости дополнительные зависимости можно установить с помощью go get.

  1. Создайте пустой файл main.go для исходного кода плагина:
touch main.go

Теперь начальная настройка завершена, и плагин готов к разработке.

Шаг 2: Структура плагина

Модуль golang.zabbix.com/sdk, установленный на предыдущем шаге, предоставляет необходимую основу для разработки плагинов и обеспечивает единообразную структуру всех плагинов.

  1. Настройте базовый поток выполнения.

Начните с определения основного потока выполнения плагина. Добавьте следующий код в main.go:

package main

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

func run() error {
    return nil
}

Это задает базовый поток выполнения для плагина. Функция run позже будет содержать основную логику плагина.

  1. Изучите интерфейсы плагина.

Плагин агента Zabbix 2 должен быть представлен структурой, которая реализует интерфейсы из пакета golang.zabbix.com/sdk/plugin:

  • Accessor - определяет основные методы, которые должны реализовывать все плагины, например задание имени плагина и обработку тайм-аутов ключей элементов данных.
  • Один или несколько из следующих функциональных интерфейсов плагина:
    • Exporter - выполняет опрос и возвращает значение (или значения), ничего или ошибку; часто используется вместе с интерфейсом Collector.
    • Collector - управляет периодическим сбором данных.
    • Runner - определяет процедуры запуска и завершения работы плагина.
    • Watcher - позволяет реализовать независимый опрос метрик, обходя внутренний планировщик агента; полезно для мониторинга на основе ловушек или событий.
    • Configurator - определяет, как плагин читает и применяет свои параметры конфигурации.

Вы можете реализовать эти интерфейсы самостоятельно или использовать стандартные, предоставляемые Zabbix Go SDK, при необходимости изменяя их. В этом руководстве используются стандартные реализации.

  1. Создайте структуру плагина.

Теперь импортируйте пакет plugin и создайте структуру myIP, которая включает структуру plugin.Base:

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

type myIP struct {
    plugin.Base
}

Структура myIP в настоящее время удовлетворяет интерфейсу Accessor. Метод для реализации одного из функциональных интерфейсов плагина, Exporter, будет добавлен позже в руководстве.

Шаг 3: Определите ключи элементов данных

Вашему плагину нужны ключи элементов данных, чтобы собирать данные и передавать их серверу Zabbix или прокси.

  1. Импортируйте пакет errs для обработки ошибок:
import "golang.zabbix.com/sdk/errs"
  1. Зарегистрируйте ключи элементов данных с помощью функции plugin.RegisterMetrics() внутри функции run():
func run() error {
    p := &myIP{}

    // Register the `myip` item key.
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // Plugin name
        "myip",                           // Item key name
        "Returns the host's IP address.", // Item key description
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    return nil
}

Чтобы зарегистрировать несколько ключей элементов данных, повторите параметры metric name и description для каждой метрики. Например:

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

Шаг 4: Настройка обработчика

Обработчик обеспечивает связь между агентом и плагином.

  1. Импортируйте пакет container:
import "golang.zabbix.com/sdk/plugin/container"
  1. Внутри функции run() добавьте код для создания и настройки обработчика:
func run() error {
    p := &myIP{}

    // Register the `myip` item key.
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // Plugin name
        "myip",                           // Item key name
        "Returns the host's IP address.", // Item key description
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    // Create a new handler.
    h, err := container.NewHandler("MyIP") // Plugin name
    if err != nil {
        return errs.Wrap(err, "failed to create new handler")
    }

    // Setup logging to forward logs from the plugin to the agent.
    // Available via p.Logger.Infof, p.Logger.Debugf, etc.
    p.Logger = h

    // Start plugin execution.
    // Blocks until a termination request is received from the agent.
    err = h.Execute()
    if err != nil {
        return errs.Wrap(err, "failed to execute plugin handler")
    }

    return nil
}

Шаг 5: Реализация сбора данных

Сбор данных выполняется через интерфейс Exporter, который описывает метод Export:

func Export(
  key string,             // Ключ элемента данных для сбора.
  params []string,        // Аргументы, передаваемые ключу элемента данных (`myip[arg1, arg2]`).
  context ContextProvider // Метаданные о сборе данных по ключу элемента данных.
) (any, error)
  1. Импортируйте необходимые пакеты для HTTP-запросов и чтения ответа:
import (
    "io"
    "net/http"
)
  1. Реализуйте метод Export для структуры myIP:
func (p *myIP) Export(
    key string, params []string, context plugin.ContextProvider,
) (any, error) {
    // Плагин может использовать разную логику сбора данных в зависимости от параметра `key`.
    // Эта реализация только проверяет, что переданный `key` поддерживается.
    if key != "myip" {
        return nil, errs.Errorf("unknown item key %q", key)
    }

    // Журнал будет перенаправлен в журнал агента 2.
    p.Infof(
        "received request to handle %q key with %d parameters",
        key,
        len(params),
    )

    // Соберите данные и верните их.

    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
}

Шаг 6: Сборка и настройка плагина

  1. Чтобы собрать плагин, выполните:
go mod tidy
go build

Это должно создать исполняемый файл myip в текущем каталоге.

  1. Настройте агент Zabbix 2 для использования плагина:
echo "Plugins.MyIP.System.Path=$PATH_TO_THE_MYIP_PLUGIN_EXECUTABLE" > /etc/zabbix_agent2.d/plugins.d/myip.conf

Замените $PATH_TO_THE_MYIP_PLUGIN_EXECUTABLE на путь к myip, созданному на шаге 5.

Имя плагина в имени параметра конфигурации (MyIP в этом руководстве) должно совпадать с именем плагина, определенным в функции plugin.RegisterMetrics().

  1. Чтобы протестировать плагин и его элемент данных myip, выполните:
zabbix_agent2 -c /etc/zabbix_agent2.conf -t myip

Вывод должен содержать внешний IP-адрес вашего узла сети и выглядеть примерно так:

myip                                          [s|192.0.2.1]

На этом вы создали простой загружаемый плагин для агента Zabbix 2. Поздравляем!

Полный исходный код

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

    // Зарегистрируйте ключ элемента данных `myip`.
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // Имя плагина
        "myip",                           // Имя ключа элемента данных
        "Возвращает IP-адрес узла сети.", // Описание ключа элемента данных
    )
    if err != nil {
        return errs.Wrap(err, "не удалось зарегистрировать метрики")
    }

    // Создайте новый обработчик.
    h, err := container.NewHandler("MyIP") // Имя плагина
    if err != nil {
        return errs.Wrap(err, "не удалось создать новый обработчик")
    }

    // Настройте журналирование для перенаправления журналов из плагина в агент.
    // Доступно через p.Logger.Infof, p.Logger.Debugf и т. д.
    p.Logger = h

    // Запустите выполнение плагина.
    // Блокируется до получения запроса на завершение от агента.
    err = h.Execute()
    if err != nil {
        return errs.Wrap(err, "не удалось выполнить обработчик плагина")
    }

    return nil
}

func (p *myIP) Export(
    key string, params []string, context plugin.ContextProvider,
) (any, error) {
    // Плагин может использовать разную логику сбора данных в зависимости от параметра `key`.
    // Эта реализация только проверяет, что указанный `key` поддерживается.
    if key != "myip" {
        return nil, errs.Errorf("unknown item key %q", key)
    }

    // Журнал будет перенаправлен в журнал агента 2.
    p.Infof(
        "получен запрос на обработку ключа %q с %d параметрами",
        key,
        len(params),
    )

    // Соберите данные и верните их.

    resp, err := http.Get("https://api.ipify.org")
    if err != nil {
        return nil, errs.Wrap(err, "не удалось получить IP-адрес")
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, errs.Wrap(err, "не удалось прочитать тело ответа")
    }

    return string(body), nil
}