EOSのリソースをZabbixで監視

# はじめに

EOS(EOSIO)では確保しているメモリやネットワーク、CPUなどのリソースが枯渇するとコントラクトの実行ができなくなります。
そこで、これらのリソースをZabbixで監視するように設定しました。

EOSのバージョンは1.8.1です。

# 作業内容
アカウントのリソースを知るにはcleosの`get account`で取得できます。
`/usr/bin/cleos --url http://localhost:8011 get account アカウント名`
（EOSを自サーバの8011ポートで起動している場合）

```bash
$ /usr/bin/cleos --url http://localhost:8011 get account アカウント名
created: YYYY-MM-DDTHH:MM:SS.sss
permissions:
     owner     1:    1 アドレス
        active     1:    1 アドレス
memory:
     quota:     742.8 TiB    used:     94.42 KiB

net bandwidth:
     delegated:1000000000.0000 SYS           (total staked delegated to account from others)
     used:               588 KiB
     available:        47.07 TiB
     limit:            47.07 TiB

cpu bandwidth:
     delegated:100000000.0000 SYS           (total staked delegated to account from others)
     used:             365.5 ms
     available:        228.6 hr
     limit:            228.6 hr


$
```
※Private環境なのでものすごく大量にリソースを割り振ってます

これをうまく取り出してZabbixに伝えれば監視ができそうです。
取り出すスクリプトを作成してzabbix_agentにたたかせることでzabbix_serverに表示させる。
という方針で作成していきます。

## スクリプト
上記で得られた指定されたアカウントのリソースをテキストファイルに出力し、
そこから監視対象項目の値を取り出すという動きにしています。

いったんテキストファイルに出力するのは
監視対象項目ごとにcleosコマンドを発行することを抑えるためです。

### zabbix-eos-stats.py

/usr/local/bin/に作成し、Ethereumユーザで実行できるようにしておきます。


```bash
cd /usr/local/bin/
vi zabbix-eos-stats.py
chmod 755 zabbix-eos-stats.py
```

中身は以下の通りです。
<details><summary>zabbix-eos-stats.pyの中身　折り畳み</summary><div>

```python:/usr/local/bin/zabbix-eos-stats.py
#!/bin/python3.6
# -*- coding: utf-8 -*-
# 指定されたアカウントのステータスを表示
# param1 : アカウント名
# param2 : 表示対象のステータス名
#
# 表示対象のステータス名は以下の通り
#   mem_quota_gb        memoryのquota(GiB単位)
#   mem_used_gb         memoryのused(GiB単位)
#   mem_available_gb    mem_quota_gb - mem_used_gb(GiB単位)
#   net_delegated       net bandwithのdelegated
#   net_used_gb         net bandwithのused(GiB単位)
#   net_available_gb    net bandwithのavailable(GiB単位)
#   net_limit_gb        net bandwithのlimit(GiB単位)
#   cpu_delegated       cpu bandwithのdelegated
#   cpu_used_min        cpu bandwithのused(分単位)
#   cpu_available_min   cpu bandwithのavailable(分単位)
#   cpu_limit_min       cpu bandwithのlimit(分単位)

import datetime
import math
import os
import re
import subprocess
import sys
import syslog

# 設定
OUTPUT_DIR = '/tmp'
FILE_PREFIX = 'zabbix-eos-stats-'
CLEOS_CMD = '/usr/bin/cleos --url http://localhost:8011'

################################################################################
class AccountInfo:

    def __init__(self):
        self.name = ''
        self.mem_quota_gb = 0
        self.mem_used_gb = 0
        self.mem_available_gb = 0
        self.net_delegated = 0
        self.net_used_gb = 0
        self.net_available_gb = 0
        self.net_limit_gb = 0
        self.cpu_delegated = 0
        self.cpu_used_min = 0
        self.cpu_available_min = 0
        self.cpu_limit_min = 0

    def parse(self, str):
        lines = str.splitlines()
        line = lines[0];                                    # created: 2020-01-01T00:00:00.000
        line = lines[1];                                    # permissions:
        line = lines[2];                                    #      owner     1:    1 EOSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        line = lines[3];                                    #         active     1:    1 EOSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        line = lines[4];                                    # memory:
        line = lines[5];  self.__parse_mem(line)            #      quota:     742.8 TiB    used:     43.56 KiB
        line = lines[6];                                    #
        line = lines[7];                                    # net bandwidth:
        line = lines[8];  self.__parse_net_delegated(line)  #      delegated:1000000000.0000 SYS           (total staked delegated to account from others)
        line = lines[9];  self.__parse_net_used(line)       #      used:               105 bytes
        line = lines[10]; self.__parse_net_available(line)  #      available:        49.92 TiB
        line = lines[11]; self.__parse_net_limit(line)      #      limit:            49.92 TiB
        line = lines[12];                                   #
        line = lines[13];                                   # cpu bandwidth:
        line = lines[14]; self.__parse_cpu_delegated(line)  #      delegated:100000000.0000 SYS           (total staked delegated to account from others)
        line = lines[15]; self.__parse_cpu_used(line)       #      used:               218 us
        line = lines[16]; self.__parse_cpu_available(line)  #      available:        252.6 hr
        line = lines[17]; self.__parse_cpu_limit(line)      #      limit:            252.6 hr

    def __parse_mem(self, line):
        """ MEMの確保量、使用量、残量をパースして算出 """
        # "     quota:     742.8 TiB    used:     43.56 KiB"
        groups = re.search('^     quota: +(([1-9]\d*|0)(\.\d+))? ([a-zA-Z]+)\s+used: +(([1-9]\d*|0)(\.\d+))? ([a-zA-Z]+) *$', line)
        if groups:
            mem_quota_val  = groups.group(1)  # 742.8
            mem_quota_unit = groups.group(4)  # TiB
            mem_used_val   = groups.group(5)  # 43.56
            mem_used_unit  = groups.group(8)  # KiB
            self.mem_quota_gb = self.__unit_exchange_to_gb(mem_quota_val, mem_quota_unit)
            self.mem_used_gb  = self.__unit_exchange_to_gb(mem_used_val,  mem_used_unit)
            self.mem_available_gb = self.mem_quota_gb - self.mem_used_gb

    def __parse_net_delegated(self, line):
        """ NETの確保量をパースして算出 """
        # "     delegated:1000000000.0000 SYS           (total staked delegated to account from others)"
        groups = re.search('^     delegated: *(([1-9]\d*|0)(\.?\d+)).* *$', line)
        if groups:
            self.net_delegated = groups.group(1)

    def __parse_net_used(self, line):
        """ NETの使用量をパースして算出 """
        # "     used:               105 bytes"
        groups = re.search('^     used: *(([1-9]\d*|0)(\.?\d+)) ([a-zA-Z]+) *$', line)
        if groups:
            net_used_val  = groups.group(1)  # 105
            net_used_unit = groups.group(4)  # bytes
            self.net_used_gb = self.__unit_exchange_to_gb(net_used_val, net_used_unit)

    def __parse_net_available(self, line):
        """ NETの残量をパースして算出 """
        # "     available:        49.92 TiB"
        groups = re.search('^     available: *(([1-9]\d*|0)(\.?\d+)) ([a-zA-Z]+) *$', line)
        if groups:
            net_available_val  = groups.group(1)  # 49.92
            net_available_unit = groups.group(4)  # TiB
            self.net_available_gb = self.__unit_exchange_to_gb(net_available_val, net_available_unit)

    def __parse_net_limit(self, line):
        """ NETの上限をパースして算出 """
        # "     limit:            49.92 TiB"
        groups = re.search('^     limit: *(([1-9]\d*|0)(\.?\d+)) ([a-zA-Z]+) *$', line)
        if groups:
            net_limit_val  = groups.group(1)  # 49.92
            net_limit_unit = groups.group(4)  # TiB
            self.net_limit_gb = self.__unit_exchange_to_gb(net_limit_val, net_limit_unit)

    def __parse_cpu_delegated(self, line):
        """ CPUの確保量をパースして算出 """
        # "     delegated:100000000.0000 SYS           (total staked delegated to account from others)"
        groups = re.search('^     delegated: *(([1-9]\d*|0)(\.?\d+)).* *$', line)
        if groups:
            self.cpu_delegated = groups.group(1) # 100000000.0000

    def __parse_cpu_used(self, line):
        """ CPUの使用量をパースして算出 """
        # "     used:               218 us"
        groups = re.search('^     used: *(([1-9]\d*|0)(\.?\d+)) ([a-zA-Z]+) *$', line)
        if groups:
            cpu_used_val  = groups.group(1)  # 218
            cpu_used_unit = groups.group(4)  # us
            self.cpu_used_min = self.__unit_exchange_to_min(cpu_used_val, cpu_used_unit)

    def __parse_cpu_available(self, line):
        """ CPUの残量をパースして算出 """
        # "     available:        252.6 hr"
        groups = re.search('^     available: *(([1-9]\d*|0)(\.?\d+)) ([a-zA-Z]+) *$', line)
        if groups:
            cpu_available_val  = groups.group(1)  # 252.6
            cpu_available_unit = groups.group(4)  # hr
            self.cpu_available_min = self.__unit_exchange_to_min(cpu_available_val, cpu_available_unit)

    def __parse_cpu_limit(self, line):
        """ CPUの上限をパースして算出 """
        # "     limit:            252.6 hr"
        groups = re.search('^     limit: *(([1-9]\d*|0)(\.?\d+)) ([a-zA-Z]+) *$', line)
        if groups:
            cpu_limit_val  = groups.group(1)  # 252.6
            cpu_limit_unit = groups.group(4)  # hr
            self.cpu_limit_min = self.__unit_exchange_to_min(cpu_limit_val, cpu_limit_unit)

    # 単位の変換
    def __unit_exchange_to_gb(self, val, unit):
        """ GiB単位に変換
        Args:
            val(float):変換対象の値
            unit(string):変換対象の単位(TiB, GiB, MiB, KiB, bytes)
        Returns:
            float: GiB単位に変換された値
        """
        if   unit == 'TiB':   rtn = float(val) * 1024
        elif unit == 'GiB':   rtn = float(val)
        elif unit == 'MiB':   rtn = float(val) / 1024
        elif unit == 'KiB':   rtn = float(val) / 1024 / 1024
        elif unit == 'bytes': rtn = float(val) / 1024 / 1024 / 1024
        else:                 rtn = -1
        return math.ceil(rtn)

    def __unit_exchange_to_min(self, val, unit):
        """ 分単位に変換
        Args:
            val(float):変換対象の値
            unit(string):変換対象の単位(hr, min, sec, ms, us)
        Returns:
            float: 分単位に変換された値
        """
        if   unit == 'hr':  rtn = float(val) * 60
        elif unit == 'min': rtn = float(val)
        elif unit == 'sec': rtn = float(val) / 60
        elif unit == 'ms':  rtn = float(val) / 60 / 1000
        elif unit == 'us':  rtn = float(val) / 60 / 1000 / 1000
        else:               rtn = -1
        return math.ceil(rtn)

    def to_list_string(self):
        lststr = []
        lststr.append("name "               + self.name                     + '\n')
        lststr.append("mem_quota_gb "       + str(self.mem_quota_gb)        + '\n')
        lststr.append("mem_used_gb "        + str(self.mem_used_gb)         + '\n')
        lststr.append("mem_available_gb "   + str(self.mem_available_gb)    + '\n')
        lststr.append("net_delegated "      + str(self.net_delegated)       + '\n')
        lststr.append("net_used_gb "        + str(self.net_used_gb)         + '\n')
        lststr.append("net_available_gb "   + str(self.net_available_gb)    + '\n')
        lststr.append("net_limit_gb "       + str(self.net_limit_gb)        + '\n')
        lststr.append("cpu_delegated "      + str(self.cpu_delegated)       + '\n')
        lststr.append("cpu_used_min "       + str(self.cpu_used_min)        + '\n')
        lststr.append("cpu_available_min "  + str(self.cpu_available_min)   + '\n')
        lststr.append("cpu_limit_min "      + str(self.cpu_limit_min)       + '\n')
        return lststr  # "[key] [value]"という文字列の配列


################################################################################
def get_accoount_info(account_name):
    """指定されたアカウントの情報を取得してファイルに出力"""
    output_file = f'{OUTPUT_DIR}/{FILE_PREFIX}{account_name}.out'
    # 最終実行から1分以内ならば終了
    if os.path.exists(output_file):
        if datetime.datetime.now() < datetime.datetime.fromtimestamp(os.stat(output_file).st_mtime) + datetime.timedelta(minutes=1):
            exit

    # cleos get account invoice
    cmd_result = subprocess.run([f'{CLEOS_CMD} get account {account_name}'], shell=True, encoding='utf-8', stdout=subprocess.PIPE)
    account_info_lines = cmd_result.stdout
    account_info = parse_account_info(account_info_lines)
    account_info.name = account_name
    # 結果をファイルに出力
    with open(output_file, 'wt') as f:
        f.writelines(account_info.to_list_string())

def parse_account_info(account_info_str):
    account_info = AccountInfo()
    account_info.parse(account_info_str)
    return account_info

def return_target_value(account_name, target_param):
    output_file = f'{OUTPUT_DIR}/{FILE_PREFIX}{account_name}.out'
    with open(output_file, 'r') as f:
        lines = f.readlines()
    for line in lines:
        if line.find(target_param) >= 0:
            # "[param] [value]"の形で記載されている
            target_value = line.split()[1]
            return target_value


################################################################################
account_name = sys.argv[1]
target_param = sys.argv[2]

# アカウント情報取得
get_accoount_info(account_name)
# 対象項目の値を出力
print(return_target_value(account_name, target_param))
```

</div></details>

`get_accoount_info()`を実行することで
リソース情報を記載した `/tmp/zabbix-eos-stats-アカウント名.out` が生成されます。

出力する項目の値はGiBもしくは分単位に統一しています。
プライベート環境で大量にリソースを割り振っているので
この単位にしていますが、お使いの環境に応じて適当に改変してください。

また、監視項目の数だけZabbixエージェントから実行されるため、
1分以内に再実行された際は、取得せずに既存のリソース情報を返すようにしています。

`return_target_value()`でそれをgrepして監視対象の項目を表示します。

テストを兼ねて一度実行してみましょう

```bash
./zabbix-eos-stats.py アカウント名 mem_quota_gb
```
正常に終了すると、以下のファイルが生成されます。

```bash:/tmp/zabbix-eos-stats-アカウント名.out
name アカウント名
mem_quota_gb 760628
mem_used_gb 1
mem_available_gb 760627
net_delegated 1000000000.0000
net_used_gb 1
net_available_gb 48200
net_limit_gb 48200
cpu_delegated 100000000.0000
cpu_used_min 1
cpu_available_min 13716
cpu_limit_min 13716
```

### Zabbixエージェントの設定
ユーザパラメータの設定ファイルを作成します。

```bash:/etc/zabbix/zabbix_agentd.d/userparameter_eos.conf
# 指定したアカウントの情報を監視する
# $1 : 監視対象のアカウント名
# $2 : 監視対象の項目名
# 監視対象の項目名は以下の通り
#   name                アカウント名
#   mem_quota_gb        memoryのquota(GiB単位)
#   mem_used_gb         memoryのused(GiB単位)
#   mem_available_gb    mem_quota_gb - mem_used_gb(GiB単位)
#   net_delegated       net bandwithのdelegated
#   net_used_gb         net bandwithのused(GiB単位)
#   net_available_gb    net bandwithのavailable(GiB単位)
#   net_limit_gb        net bandwithのlimit(GiB単位)
#   cpu_delegated       cpu bandwithのdelegated
#   cpu_used_min        cpu bandwithのused(分単位)
#   cpu_available_min   cpu bandwithのavailable(分単位)
#   cpu_limit_min       cpu bandwithのlimit(分単位)
#
UserParameter=eos.getinfo[*],/usr/local/bin/zabbix-eos-stats.py $1 $2
```
作成したら読み込ませるためzabbix-agentを再起動しましょう。

## Zabbixサーバの設定
### アプリケーション
アプリケーションとして`EOS`を作成します。

### アイテム
アイテムを作成します。

|項目|値|備考|
|----|----|----|
|名前|resource __アカウント名__ __mem_quota_gb__|監視対象項目に応じて適当に|
|タイプ|Zabbixエージェント||
|キー|eos.getinfo[__アカウント名__, __mem_quota_gb__]|監視対象項目に応じて適当に|
|ホストインターフェース|監視対象のもの||
|データ型|数値（浮動小数）||
|単位|GiB もしくは min||
|アプリケーション|EOS||

上記以外の項目はそれぞれの環境に合わせて適当に設定します。
値が取れれば成功です！

テンプレートとして作ったりトリガー設定などは適当に。。。