Zabbixのローダブルモジュールの便利機能で監視データをInfluxDBにダイレクトに保存

Zabbixのローダブルモジュールとは、ZabbixサーバやZabbixエージェントに対して内部にCで実装した独自プログラムを組み込んで監視機能を拡張できる機能です。

Cで実装する以外にも以下のページで紹介しているようにGo言語で実装したものをモジュール化して読み込むといったこともできます。

* [Golangを使ってZabbixを拡張](https://qiita.com/ike_dai/items/b4e92a6403b7e1467d2d)

このローダブルモジュールの機能がZabbix3.2以降で更に拡張されており、***監視結果のヒストリデータを外部に取り出して処理***させることができるようになっています。

Zabbixの監視結果のヒストリデータはhistory syncerプロセスによりZabbix管理用のデータベース(MySQLやPostgreSQL、Oracle等)にデータ格納しますが、この処理の中に自分で拡張したコードを差し込めるようになっています。

この機能を活用してZabbixで監視してきたデータを時系列データベースのInfluxDBに格納してみます。

# InfluxDBへのデータ登録方法

InfluxDBへのデータ登録は/write WebAPIで行います。
詳細は[こちら](https://docs.influxdata.com/influxdb/v1.6/tools/api/#write-http-endpoint)に記載の通りです。
InfluxDBのCのライブラリが公開されているのですが、メンテはされていないようで利用には注意が必要そうです。

* [influxdb-c](https://github.com/influxdata/influxdb-c)

今回は単純に監視結果のヒストリデータをwriteするだけなので、このライブラリは利用せず、ZabbixのWeb監視等でも利用されているcurlのCライブラリlibcurlを使ってwriteのAPIをPOSTするようなコードで対処することにします。

write APIの使い方は以下のような感じです。
zabbixという名称のInfluxDBのデータベースに、itemidのタグ付けをして、valueカラムに値を登録する場合の例です。
コマンドライン上で以下のように実行すればinfluxdbに値が登録できます。

```
$ curl -XPOST "http://influxdb-server:8086/write?db=zabbix&precision=ns" --data-binary 'log,itemid=123 value="test data"'
```

また、一度のAPI呼び出しで複数のデータをPOSTしたい場合には、複数のデータを改行コードを挟んで記載することで対応できます。

```
$ curl -XPOST "http://influxdb-server:8086/write?db=zabbix&precision=ns" --data-binary $'log,itemid=123 value="test data"\nlog,itemid=234 value="test data2"'
```

改行コードをPOSTデータに乗せるため、curlのオプションとしては--data-binaryを使っています。

# ローダブルモジュールを実装

Zabbixのソースコード内にあるsrc/modules/dummyを参考にして実装するために、dummyフォルダを複製して、influxdb-storeというフォルダを作ります。dummy.cをrenameしてinfluxdb_store.cを作成します。

```
$ cp -r src/modules/dummy src/modules/influxdb-store
$ mv src/modules/influxdb-store/dummy.c src/modules/influxdb-store/influxdb_store.c
```

influxdb_store.cは以下のように実装します。

```c:influxdb_store.c
#include "sysinc.h"
#include "module.h"
#include "common.h"
#include "log.h"

static int	item_timeout = 0;

static void	write_influx(char *data);

static ZBX_METRIC keys[] =
{
	{NULL}
};

int	zbx_module_api_version(void)
{
	return ZBX_MODULE_API_VERSION;
}

void	zbx_module_item_timeout(int timeout)
{
	item_timeout = timeout;
}

ZBX_METRIC	*zbx_module_item_list(void)
{
	return keys;
}

int	zbx_module_init(void)
{
	return ZBX_MODULE_OK;
}

int	zbx_module_uninit(void)
{
	return ZBX_MODULE_OK;
}

static void	influx_history_float_cb(const ZBX_HISTORY_FLOAT *history, int history_num)
{
	int	i;
	char *data;

	for (i = 0; i < history_num; i++)
	{
		data = zbx_dsprintf(data, "%s\nfloat_type,itemid=%d value=%f", data, history[i].itemid, history[i].value);
	}
	write_influx(data);
}

static void	influx_history_integer_cb(const ZBX_HISTORY_INTEGER *history, int history_num)
{
	int	i;
	char *data;

	for (i = 0; i < history_num; i++)
	{
		data = zbx_dsprintf(data, "%s\nint_type,itemid=%d value=%d", data, history[i].itemid, history[i].value);
	}
	write_influx(data);
}

static void	influx_history_string_cb(const ZBX_HISTORY_STRING *history, int history_num)
{
	int	i;
	char *data;

	for (i = 0; i < history_num; i++)
	{
		data = zbx_dsprintf(data, "%s\nstr_type,itemid=%d value=\"%s\"", data, history[i].itemid, history[i].value);
	}
	write_influx(data);
}

static void	influx_history_text_cb(const ZBX_HISTORY_TEXT *history, int history_num)
{
	int	i;
	char *data;

	for (i = 0; i < history_num; i++)
	{
		data = zbx_dsprintf(data, "%s\ntext_type,itemid=%d value=\"%s\"", data, history[i].itemid, history[i].value);
	}
	write_influx(data);
}

static void	influx_history_log_cb(const ZBX_HISTORY_LOG *history, int history_num)
{
	int	i;
	char *data;

	for (i = 0; i < history_num; i++)
	{
		data = zbx_dsprintf(data, "%s\nlog_type,itemid=%d value=\"%s\"", data, history[i].itemid, history[i].value);
	}
	write_influx(data);
}

static void	write_influx(char *data)
{
	CURL *curl;

    	curl = curl_easy_init();
    	curl_easy_setopt(curl, CURLOPT_URL, "http://influxdb-server:8086/write?db=zabbix&precision=ns");
    	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    	curl_easy_setopt(curl, CURLOPT_POST, 1);
    	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);

    	curl_easy_perform(curl);

    	curl_easy_cleanup(curl);
}


ZBX_HISTORY_WRITE_CBS	zbx_module_history_write_cbs(void)
{
	static ZBX_HISTORY_WRITE_CBS	influx_callbacks =
	{
		influx_history_float_cb,
		influx_history_integer_cb,
		influx_history_string_cb,
		influx_history_text_cb,
		influx_history_log_cb,
	};

	return influx_callbacks;
}
```

ヒストリデータの書き込みはzbx_module_history_write_cbsという関数で定義します。
この中で各データ型毎にどのcallback関数を呼び出すかを定義することで、処理の実体の関数に飛ばされます。ログ型の場合だと、influx_history_log_cb関数といった具合です。あとはその処理の実体の関数内で、受け取ったhistoryのデータを加工してinfluxdbに書き込むような処理を実装すればOKです。

# ローダブルモジュールのビルド

事前にgccやautoconf、automake、libcurl-devel(curlライブラリを利用するため)をインストールします。
CentOS7の場合の例です。

```
$ sudo yum install gcc autoconf automake libcurl-devel
```

Zabbixのソースコードの全体で./configureを実行します。このとき、libcurlを使う前提でconfigure実行します。

```
$ ./configure --with-libcurl
```

Makefileを修正します。

```c:Makefile
influxdb_store: influxdb_store.c
	gcc -fPIC -shared -o influxdb_store.so influxdb_store.c -I../../../include

```

makeを実行してビルド。

```
$ cd src/modules/influxdb-store
$ make
```

これでinfluxdb_store.soファイルが生成されます。
このファイルをzabbix_server.confのLoadModulePathで指定する箇所に配置します。

# InfluxDBにZabbixデータ格納用のデータベースを作成

作成したローダブルモジュールの例では、zabbixというデータベース上にlog_type、float_type、text_type、str_type、int_typeというmeasurement名でそれぞれの型のデータを格納するように作成しています。
そのため、事前にzabbixデータベースを作成しておきます。

influx cliを用いる場合は以下のように実施します。

```
$ influx
> create database zabbix;
```

# Zabbix Serverの設定変更と再起動

ローダブルモジュールを読み込む設定をzabbix_server.confに行います。
LoadModuleに先程作成したinfluxdb_store.soを指定します。

```zabbix_server.conf
LoadModulePath=/var/lib/zabbix/modules
LoadModule=influxdb_store.so
```

あとはzabbix serverを再起動すれば各型の監視結果が保存されるたびにInfluxDBのzabbixデータベースにデータが格納されていきます。

# 格納されたデータの確認

監視データが入ってくると、InfluxDBのzabbixデータベース上にint_type,float_type,text_type,log_type,str_typeといった各measurementにデータが格納されます。今回の実装例の場合だと、各measurementにtagとしてitemidを付与し、valueフィールドに実際の監視データを格納しています。
Influx cliから確認すると以下のような感じです。

```
$ influx
> use zabbix
> show series;
key
---
float_type,itemid=23252
float_type,itemid=23253
・・・略
int_type,itemid=23255
int_type,itemid=23256
・・・略
log_type,itemid=28296
・・・略
str_type,itemid=23288
str_type,itemid=23307
・・・略
> SELECT * FROM float_type WHERE "itemid" = '23253';
name: float_type
time                itemid value
----                ------ -----
1537269694859082180 23253  0
1537269753018962324 23253  0.152181
1537269813170550179 23253  0.033864
1537269873352042233 23253  0.016935
・・・略
> SELECT * FROM log_type WHERE "itemid" = '28296';
name: log_type
time                itemid value
----                ------ -----
1537269051485495673 28296  2018-09-04 17:40:00 [Warning] error log sample
1537269051515371621 28296  2018-09-04 17:40:00 [Warning] error log sample
1537269051528303541 28296  2018-09-04 17:40:00 [Warning] error log sample
1537269051537937326 28296  2018-09-04 17:40:00 [Warning] error log sample
1537269318720193729 28296  2018-09-04 17:40:00 [ERROR] error log sample
```

このようにInfluxDBにデータを格納できると、InfluxDBの時系列分析の関数が簡単につかえて便利です。
例えば、InfluxDBのInfluxQLには関数として***[HOLT_WINTERS()](https://docs.influxdata.com/influxdb/v1.6/query_language/functions/#holt-winters)***という数値データの時系列の予測関数があったりします。これを使えば、InfluxDBに蓄積された監視データを使って、以下のように簡単に予測値を求めることもできるようになります。

```
> SELECT HOLT_WINTERS(FIRST("value"),10,4) FROM float_type WHERE "itemid"='23253' AND time >= '2018-09-18 00:00:00' AND time <= '2018-09-19 00:00:00' GROUP BY time(5m)
name: float_type
time                holt_winters
----                ------------
1537271400000000000 0.0182841742495617
1537271700000000000 0.03465164292049208
1537272000000000000 0.0378210329676341
1537272300000000000 0.04011877172366822
1537272600000000000 0.041776201720146025
1537272900000000000 0.03157544553282758
1537273200000000000 0.03184417204227742
1537273500000000000 0.016704778606314058
1537273800000000000 0.011141683294623835
1537274100000000000 0.007111702114006886
```

この例だと、itemidが23253のfloat_typeの監視結果のvalueデータを、5分毎にグループ化し、今後10個の予測値を求めている例です。数値データの推移傾向から周期性(この例の場合は4個分を周期と設定している)を考慮しつつ簡単に予測値が求められます。

# まとめ

このようにZabbixの監視データを時系列の分析が行いやすい外部のデータストアに格納することで監視運用の幅がより広がるかと思います。
ローダブルモジュールは不具合があってクラッシュすると、ロード元のZabbix Server全体がクラッシュすることにつながるので、利用の際には十分ご注意を。