创建插件

本页面面向有兴趣为 Zabbix agent 2 开发自有插件(独立数据采集器)的用户。

自定义插件可将 Zabbix agent 2 的功能扩展到内置插件可加载插件所提供范围之外。

每个插件都是一个 Go 软件包,用于定义结构并实现一个或多个接口(ExporterConfiguratorRunner)。更多信息请参见插件接口连接图

本指南将帮助您创建一个自定义可加载插件。

如需更多指导,另请参见以下仓库:

您将创建的内容

这是一个分步教程,用于创建一个名为 MyIP 的简单可加载插件。
该插件将实现一个单独的监控项键(myip),用于返回 Zabbix agent 主机的外部 IP 地址。

第 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 替换为 golang.zabbix.com/sdk repository 相应发布分支中最新 HEAD 提交的哈希值。 例如:

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 agent 2 插件应由一个结构体表示,该结构体实现 golang.zabbix.com/sdk/plugin 包中的接口:

  • Accessor - 定义所有插件都必须实现的基本方法,例如设置插件名称以及处理监控项键超时。
  • 以下一个或多个功能性插件接口:
    • Exporter - 执行一次轮询并返回一个值(或多个值)、不返回任何内容,或返回错误;通常与 Collector 接口一起使用。
    • Collector - 管理数据的周期性采集。
    • Runner - 定义插件的启动和关闭过程。
    • Watcher - 允许实现独立的指标轮询,绕过 agent 的内部调度器;适用于基于 trap 或事件驱动的监控。
    • Configurator - 定义插件如何读取并应用其配置设置。

您既可以自行实现这些接口,也可以使用 Zabbix Go SDK 提供的默认实现,并根据需要进行修改。
本教程使用默认实现。

  1. 创建插件结构体。

现在,导入 plugin 包并创建一个嵌入 plugin.Base 结构体的 myIP 结构体:

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

type myIP struct {
    plugin.Base
}

目前,myIP 结构体满足 Accessor 接口。
在本教程的后续内容中,将添加一个用于实现功能性插件接口之一 Exporter 的方法。

第 3 步:定义监控项键值

您的插件需要监控项键值来收集数据,并将其提供给 Zabbix 服务器或 proxy。

  1. 导入 errs 包以进行错误处理:
import "golang.zabbix.com/sdk/errs"
  1. run() 函数中使用 plugin.RegisterMetrics() 函数注册监控项键值:
func run() error {
    p := &myIP{}

    // 注册 `myip` 监控项键值。
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // 插件名称
        "myip",                           // 监控项键值名称
        "Returns the host's IP address.", // 监控项键值描述
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    return nil
}

要注册多个监控项键值,请为每个指标重复使用参数 metric namedescription
例如:

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

第 4 步:设置处理程序

处理程序用于促进 agent 与插件之间的通信。

  1. 导入 container 包:
import "golang.zabbix.com/sdk/plugin/container"
  1. run() 函数中添加用于创建和设置处理程序的代码:
func run() error {
    p := &myIP{}

    // 注册 `myip` 监控项键。
    err := plugin.RegisterMetrics(
        p,
        "MyIP",                           // 插件名称
        "myip",                           // 监控项键名称
        "返回主机的 IP 地址。",            // 监控项键描述
    )
    if err != nil {
        return errs.Wrap(err, "failed to register metrics")
    }

    // 创建新的处理程序。
    h, err := container.NewHandler("MyIP") // 插件名称
    if err != nil {
        return errs.Wrap(err, "failed to create new handler")
    }

    // 设置日志记录,将日志从插件转发到 agent。
    // 可通过 p.Logger.Infof、p.Logger.Debugf 等方式使用。
    p.Logger = h

    // 启动插件执行。
    // 在收到来自 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. myIP 结构体实现 Export 方法:
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)
    }

    // 日志将被转发到 agent 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 agent 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 替换为步骤 5 中创建的 myip 的路径。

配置参数名称中的插件名称(本教程中为 MyIP)必须与 plugin.RegisterMetrics() 函数中定义的插件名称一致。

  1. 要测试该插件及其 myip 监控项,请运行:
zabbix_agent2 -c /etc/zabbix_agent2.conf -t myip

输出应包含您的主机的外部 IP 地址,并且看起来类似于:

myip                                          [s|192.0.2.0]

至此,您已经为 Zabbix agent 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, "failed to register metrics")
    }

    // 创建一个新的处理程序。
    h, err := container.NewHandler("MyIP") // 插件名称
    if err != nil {
        return errs.Wrap(err, "failed to create new handler")
    }

    // 设置日志记录,将插件中的日志转发到 agent。
    // 可通过 p.Logger.Infof、p.Logger.Debugf 等使用。
    p.Logger = h

    // 启动插件执行。
    // 在收到来自 agent 的终止请求之前会一直阻塞。
    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) {
    // 插件可以根据 `key` 参数使用不同的数据采集逻辑。  
    // 此实现仅验证提供的 `key` 是否受支持。 
    if key != "myip" {
        return nil, errs.Errorf("unknown item key %q", key)
    }

    // 日志将被转发到 agent 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
}