Writing a Zabbix module with cgo
So first thing out of the way, if you want to write a Zabbix module the easy way with Go, I’ve done all of the hard work for you and packaged it up as g2z. G2z lets you write a module in pure Go without having to touch a line of C code and with a fully documented API. Nice!
If you prefer the road less traveled, this article will describe how to write a module using cgo. The result will be a shared C library (.so) written in Go which extends Zabbix by exposing the required C functions. The complete example which re-implements the dummy C module published by Zabbix is attached below.
Looking to write Zabbix modules in Rust? Check out Marin Atanasov Nikolov’s blog
This article assumes you are competent with C, Go and familiar with writing Zabbix modules in C. My intention is to save you the hassle of solving some of the problems I encountered trying to write a module (and the g2z adapter) in Go.
Comments and questions are welcomed below!
Table of contents
- Building a shared library
- Loading a Zabbix module
- Mandatory module interface
- Defining items
- Getting parameter values
- Returning a value
- Unsupported items
- Calling Zabbix functions
- Complete example
You’re going to need the following prerequisites:
Zabbix v2.2.0 or above (agent and sources)
Go v1.5 or above
GNU build tools
It’s also worthwhile to familiarize yourself with some background concepts:
Interfacing between C and Go with cgo
Writing Zabbix loadable modules in C
Writing Go shared libraries
Building a shared library
Go v1.5 and above has the capability to build shared libraries which can expose functions to C. To successfully compile a shared library in Go:
Define a mandatory
Import cgo via
build with the
C package (a.k.a
cgo) exposes C APIs to Go and allows us to use functions and constants
defined in the Zabbix C header files. It also allows us to expose Go functions to C (i.e. the Zabbix
agent) using the
//export directive (note no spaces after
As a shortcut to build your module, the following
Makefile will save you some time:
make from your project directory to compile the module.
Loading a Zabbix module
You’ll need to load your module into the Zabbix agent using the
configuration directives. Please see the Zabbix documentation for further details.
Once loaded, there are three ways to test your custom item checks:
Test an individual key with
zabbix_agentd -t <key>
Test all keys using their test parameters with
Test an individual key against a daemonized agent with
zabbix_get -s <host> -k <key>. Be sure to restart the agent to reload your module each time you recompile it before running tests.
Mandatory module interface
Let’s start by satisfying the minimum interace for Zabbix to be able to load the module. This means
zbx_module_init(). Note the
above each function to expose them to C.
We’re also going to use some constants defined in the Zabbix sources in
include/module.h. To call
C code (in this case to import
module.h and its prerequisites) we need to create a cgo preamble
which is simply C code encapsulated in Go comments (
/* */), immediately preceeding
with no additional whitespace or line breaks.
zbx_module_init() to execute actions when the Zabbix agent loads your module. Actions might
include starting a timer or tailing a log file.
Only one version of the module API is currently supported so
Note below we’ve included
zbx_module_item_list(). Despite what the Zabbix documentation says, the
agent won’t load unless this function is also defined. We’ll come back to implementing this function
Compile your module with
make. You can see the exported functions by running
$ nm -gl <module>.so 000000000005ef40 T zbx_module_api_version /tmp/go-build120687282/_/usr/src/zbx/_obj/_cgo_export.c:9 000000000005ef70 T zbx_module_init /tmp/go-build120687282/_/usr/src/zbx/_obj/_cgo_export.c:21 000000000005efa0 T zbx_module_item_list /tmp/go-build120687282/_/usr/src/zbx/_obj/_cgo_export.c:33
Restart your Zabbix agent. You should see an entry in the agent log file similar to
loaded modules: <module>.so
You may also optionally implement
zbx_module_uninit() to execute actions when Zabbix unloads the
zbx_module_item_timeout() to retrieve the configured timeout to obey for all item
Each item in your module is defined in a
C.ZBX_METRIC structure which includes an item ‘key’, test
parameters, configuration flags and a C function to call when the agent queries the item. All of
your items must be registered to the Zabbix agent when it calls the
function in your module. This function must return a
NULL terminated array of metric structs.
To pass a pointer to your handler function to Zabbix, we need to tell Go how to cast it to C. Add the following typedef in your C preample:
Each item you define must have a matching, exported function with the following Go signature:
The C signature for the function must also be declared in the preamble as:
Add each item to the array returned by
metrics array length should
be the number of items exported, plus one (the
Use the exported name of your item callback function in the
function field (notice we cast it
agent_item_handler typedef from earlier).
If your item accepts parameters, set
To pass test parameters to your item when
zabbix_agentd -p is called, specify a comma separated
list of parameters in
You may observe that
C.CStringallocates memory on the heap which is not cleaned up. This function is only called once and the return value persists for the life of the agent PID so I don’t consider this a problem. Please feel free to convince me otherwise.
Because Go slices include an additional header compared with C arrays, we return the address of the first element in the slice, rather than the slice iteself.
Getting parameter values
get_rparam (defined in
module.h) is used in Zabbix C code to retrieve a key parameter
from an agent request. While Go does support pre-compiler macros, the implementation for
get_rparam doesn’t unpack in Go.
To solve this, we could implement
get_rparam directly in Go but we could run into upgrade problems
if Zabbix ever change their implementation. It’s also not very pleasant trying to index a
Instead we’ll just create a C wrapper function in the preamble.
Now you can use the following to retrieve a zero-indexed request parameter in your item functions:
To validate the number of parameters passed to an item, just compare
Returning a value
When writing a module in C, you would use the
SET_*_RESULT() function macros from
set the return value and type on the
AGENT_RESULT struct. Once again, unfortunately, these macros
don’t unpack into Go so we need to add some wrapper functions to the C preamble.
You only need to define wrapper functions for the return types you intend to use.
You can now set a return value on the result struct in your handler function like so:
If you need to raise an error in your handler functions (i.e.
ZBX_NOT_SUPPORTED), simply return
C.SYSINFO_RET_FAIL. If you would like to also set an optional error message (which appears in the
Zabbix web console on the ‘Not Supported’ error icon), you can use the
wrapper described above.
Calling Zabbix functions
So far our module exposes a bunch of functions with bindings for C so the Zabbix agent can load and
call our Go code. While not essential, it can be useful to call C functions inside Zabbix from Go.
One example use case is to call
zabbix_log() for writing messages to the Zabbix agent log file.
The primary complication in calling Zabbix functions is that during compilation or when executing
go test), the Zabbix function symbols cannot be resolved (because they are not in a
shared module and your module is not yet loaded in Zabbix).
For example, if you attempt to call
zabbix_log() (which is a macro for
will see a compilation error similar to the following:
/tmp/go-build376688079/_/usr/src/g2z/direct/_obj/dummy.cgo2.o: In function `cgo_zabbix_log': ./dummy.go:43: undefined reference to `__zbx_zabbix_log'
To resolve this issue, we need to tell the linker to ignore missing symbols by including the following in the C preamble:
If you happen to be compiling on OS X, use the following LDFLAGS instead:
-flat_namespace -undefined suppress
These flags tell the linker to allow unresolved symbols at compile time, assuming they will be
available at runtime. Unfortunately, this doesn’t help us when running
go test which will
load our module independently of Zabbix, meaning the symbols will also fail to resolve at runtime.
To resolve this, we need to do some runtime checks to see if the symbols can resolve (i.e. the
module was loaded by Zabbix) or to fail gracefully if they can not (loaded by
Firstly, we need to tell the compiler to allow calls to the Zabbix symbols we wish to consume, even
if they are undefined with
#pragma weak. We need to do this for each function and be sure to use
the actual function name, not the convenience macros (e.g.
log.h from the Zabbix sources).
Next, we need to create a wrapper in the preamble that performs a runtime check to test a function pointer and make sure it is non-zero. In this case, if the symbol does not resolve, our wrapper function does nothing.
This function also performs another important function.
zabbix_log() is a variadic function (i.e.
it accepts a variable number of parameters via
...). Unfortunately cgo does not support calling
variadic C functions so all other variadic functions will also need to be wrapped with a
non-variadic C function in your preamble.
Now that you can call the
zabbix_log function inside Zabbix, you can create a convenience wrapper
in Go to allow for variadic formatting via
fmt.Sprintf() and to handle freeing any allocated