Prometheus snmp или как я писал свой exporter…

Встала необходимость поставить на мониторинг QNAP, гугл ответил что есть прекрасный prometheus snmp exporter https://github.com/prometheus/snmp_exporter, зашел, почитал, в целом все понятно. Что нужно? Нужно создать конфигурацию на основе MIB с помощью так называемого SNMP Exporter Config Generator, его надо собрать на Go или воспользоваться docker контейнером, все это описано в репозитории. Я скачал необходимый мне MIB, собрал генератор и…… И пришло разочарование, потому как половина значений которые сгенерировались при запуске экспортера возвращались как метки, а не как значения. В целом, можно выбирать метки как значения через Grafana, но, что по мне, это лишняя работа. К тому же, после генерации, я не обнаружил необходимых мне метрик в файле. Хорошо, пошли искать в интернетах какие OID-ы вообще есть в QNAP, вот что нашлось https://bestmonitoringtools.com/mibdb/mibdb_search.php?mib=NAS-MIB. Хорошо, значит корневой oid будет 1.3.6.1.4.1.24681, а значит можно посмотреть и значения. Для этого нам потребуется утилита snmpwalk, она входит в пакет snmp практически всех linux дистрибутивов. Как её установить, думаю вы найдете в интернете, чтобы не нагромождать статью. И так смотрим что нам выведет дерево:

# snmpwalk -v2c -c public ip_qnap 1.3.6.1.4.1.24681

SNMPv2-SMI::enterprises.24681.1.2.1.0 = STRING: "4.5 %"
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.1 = INTEGER: 1
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.2 = INTEGER: 2
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.3 = INTEGER: 3
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.4 = INTEGER: 4
SNMPv2-SMI::enterprises.24681.1.2.9.1.1.5 = INTEGER: 5
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.1 = STRING: "eth0"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.2 = STRING: "eth1"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.3 = STRING: "eth2"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.4 = STRING: "eth3"
SNMPv2-SMI::enterprises.24681.1.2.9.1.2.5 = STRING: "eth4"
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.1 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.2 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.3 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.4 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.3.5 = Counter32: 3068842230
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.1 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.2 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.3 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.4 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.4.5 = Counter32: 2780806888
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.1 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.2 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.3 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.4 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.9.1.5.5 = Counter32: 0
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.1 = INTEGER: 1
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.2 = INTEGER: 2
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.3 = INTEGER: 3
SNMPv2-SMI::enterprises.24681.1.2.11.1.1.4 = INTEGER: 4
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.1 = STRING: "HDD1"
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.2 = STRING: "HDD2"
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.3 = STRING: "HDD3"
SNMPv2-SMI::enterprises.24681.1.2.11.1.2.4 = STRING: "HDD4"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.1 = STRING: "32 C/89 F"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.2 = STRING: "32 C/89 F"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.3 = STRING: "32 C/89 F"
SNMPv2-SMI::enterprises.24681.1.2.11.1.3.4 = STRING: "31 C/87 F"
[skip]

Список огромный, хорошо. Теперь у нас есть данные, но, данные у нас динамические, если вдруг я уберу диск из корзины их станет 3 или 2, или например подключу ещё один сетевой порт, и так далее. Задача ясна, но сначала надо написать сам экспортер который будет брать эти данные и наполнять ими прометей. Так как я пишу больше на Python, то, идем в документацию: https://github.com/prometheus/client_python, вдумчиво читаем и понимаем что все проще простого. Мы не будем углубляться в этой статье о типах данных в прометее, скажу лишь что я взял изменяемый тип, как написано в документации: Gauges can go up and down. Это то, что нужно! Что же дальше, а дальше нам надо взять экземпляр этого класса на его основе создать свой и суть кроется в метках (lablesname), мы создаем один экземпляр на каждую метрику, но в каждой метрике может быть сколь угодно конечных точек. Например, мы создаем метрику для HDD, но у нас их 4, а может быть 6 и так далее, по этому, мы создаем только одну метрику, а то количество дисков мы будем помещать в метки, думаю по коду это будет понятно. Хорошо. Сначала научим Python забирать данные по snmp. Библиотека pysnmp, вполне подойдет, pip install pysnmp.

from pysnmp.hlapi import *
import sys


snmp_host = 'ip_qnap'

# Я взял основные UID которые мне показались самыми важными на момент написания,
# ничто не мешает добавить по вкусу то, что считаете необходимым. 

oid_list = [
    '1.3.6.1.4.1.24681.1.3.1',                  # CPU usage
    '1.3.6.1.4.1.24681.1.3.9.1',                # Ethernet params
    '1.3.6.1.4.1.24681.1.3.11.1',               # HDD params
    '1.3.6.1.4.1.24681.1.3.15.1',               # Fan params
    '1.3.6.1.4.1.24681.1.3.2',                  # Total memory
    '1.3.6.1.4.1.24681.1.3.3',                  # Free memory
    '1.3.6.1.4.1.24681.1.3.5',                  # CPU temperature
    '1.3.6.1.4.1.24681.1.3.6',                  # System temperature
    '1.3.6.1.4.1.24681.1.2.13',                 # System name
    '1.3.6.1.4.1.24681.1.2.12'                  # Model name
]


def walk(host, oid):
    snmp_response = []
    for (errorIndication,
         errorStatus,
         errorIndex,
         varBinds) in nextCmd(SnmpEngine(),
                              CommunityData('public'),
                              UdpTransportTarget((host, 161)),
                              ContextData(),
                              ObjectType(ObjectIdentity(oid)),
                              lookupMib=False,
                              lexicographicMode=False):

        if errorIndication:
            print(errorIndication, file=sys.stderr)
            break

        elif errorStatus:
            print('%s at %s' % (errorStatus.prettyPrint(),
                                errorIndex and varBinds[int(errorIndex) - 1][0] or '?'), file=sys.stderr)
            break
        else:
            for varBind in varBinds:
                snmp_response.append((str(varBind[0]), str(varBind[1])))

    return_value = {}
    for i in snmp_response:
        return_value.setdefault(i[0].split('.')[-1], []).append(i[1])
    return_value.update({'oid': [oid]})
    return return_value

На выходе получаем словарь со списками данных:

PyDev console: starting.
Python 3.9.6 (default, Sep 26 2022, 11:37:49) 
[Clang 14.0.0 (clang-1400.0.29.202)] on darwin

>>> from snmp_exporter import walk
>>> print(walk('qnap_ip', '1.3.6.1.4.1.24681.1.3.15.1'))

{'1': ['1', 'System FAN 1', '6849'], '2': ['2', 'System FAN 2', '6849'], 'oid': ['1.3.6.1.4.1.24681.1.3.15.1']}

Отлично, то что надо, остается только отдать это прометею, для этого импортируем библиотеку прометея pip install prometheus-client после чего можно создавать наши метрики:

from prometheus_client import start_http_server, Gauge

prometheus_metric_cpu = Gauge('cpu', 'CPU utilization', labelnames=['instance', 'cpu'])
prometheus_metric_network_in = Gauge('network_incomming', 'Network Incoming stack', labelnames=['instance', 'id', 'if_name'])
prometheus_metric_network_out = Gauge('network_output', 'Network Output stack', labelnames=['instance', 'id', 'if_name'])
prometheus_metric_hdd_temperature = Gauge('hdd_temperature', 'HDD stack', labelnames=['instance', 'id', 'name', 'model', 'state'])
prometheus_metric_hdd_capacity = Gauge('hdd_capacity', 'HDD stack', labelnames=['instance', 'id', 'name', 'model', 'state'])
prometheus_metric_fan = Gauge('system_fan_speed', 'FAN stack', labelnames=['instance', 'id', 'name'])
prometheus_metric_memory_total = Gauge('memory_total', 'Memory stack', labelnames=['instance'])
prometheus_metric_memory_fee = Gauge('memory_free', 'Memory stack', labelnames=['instance'])
prometheus_metric_cpu_temperature = Gauge('cpu_temperature', 'CPU temperature', labelnames=['instance'])
prometheus_metric_system_temperature = Gauge('system_temperature', 'System Temperature', labelnames=['instance'])
prometheus_metric_system_name = Gauge('system_name', 'System name', labelnames=['instance', 'systemname'])
prometheus_metric_model_name = Gauge('model_name', 'Model name', labelnames=['instance', 'modelname'])

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

if __name__ == '__main__':
    start_http_server(8001)
    while True:
        for oid in oid_list:
            get_all_oid = walk(snmp_host, oid)
            if get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.1':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_cpu.labels(snmp_host, 'cpu').set(get_value[0])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.9.1':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_network_in.labels(snmp_host, get_value[0], get_value[1]).set(get_value[2])
                    prometheus_metric_network_out.labels(snmp_host, get_value[0], get_value[1]).set(get_value[3])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.11.1':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_hdd_temperature.labels(snmp_host, get_value[0], get_value[1], get_value[4], get_value[6]).set(get_value[2])
                    prometheus_metric_hdd_capacity.labels(snmp_host, get_value[0], get_value[1], get_value[4], get_value[6]).set(get_value[5])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.15.1':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_fan.labels(snmp_host, get_value[0], get_value[1]).set(get_value[2])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.2':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_memory_total.labels(snmp_host).set(get_value[0])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.3':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_memory_fee.labels(snmp_host).set(get_value[0])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.5':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_cpu_temperature.labels(snmp_host).set(get_value[0])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.3.6':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_system_temperature.labels(snmp_host).set(get_value[0])
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.2.13':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_system_name.labels(snmp_host, get_value[0]).set(1)
            elif get_all_oid.get('oid')[0] == '1.3.6.1.4.1.24681.1.2.12':
                get_all_oid.pop('oid')
                for get_value in get_all_oid.values():
                    prometheus_metric_model_name.labels(snmp_host, get_value[0]).set(1)
        time.sleep(15)

Вот собственно и все, порт 8001 доступен и можно зайти и посмотреть данные:

# curl http://localhost:8001/

Out:

# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 3.507817e+06
python_gc_objects_collected_total{generation="1"} 677080.0
python_gc_objects_collected_total{generation="2"} 4.8128355e+07
# HELP python_gc_objects_uncollectable_total Uncollectable object found during GC
# TYPE python_gc_objects_uncollectable_total counter
python_gc_objects_uncollectable_total{generation="0"} 0.0
python_gc_objects_uncollectable_total{generation="1"} 0.0
python_gc_objects_uncollectable_total{generation="2"} 0.0
# HELP python_gc_collections_total Number of times this generation was collected
# TYPE python_gc_collections_total counter
python_gc_collections_total{generation="0"} 208464.0
python_gc_collections_total{generation="1"} 18951.0
python_gc_collections_total{generation="2"} 1722.0
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="9",patchlevel="6",version="3.9.6"} 1.0
# HELP cpu CPU utilization
# TYPE cpu gauge
cpu{cpu="cpu",instance="ip_qnap"} 2.0
# HELP network_incomming Network Incoming stack
# TYPE network_incomming gauge
network_incomming{id="1",if_name="eth0",instance="ip_qnap"} 0.0
network_incomming{id="2",if_name="eth1",instance="ip_qnap"} 0.0
network_incomming{id="3",if_name="eth2",instance="ip_qnap"} 0.0
network_incomming{id="4",if_name="eth3",instance="ip_qnap"} 0.0
network_incomming{id="5",if_name="eth4",instance="ip_qnap"} 3.120115368e+09
# HELP network_output Network Output stack
# TYPE network_output gauge
network_output{id="1",if_name="eth0",instance="ip_qnap"} 0.0
network_output{id="2",if_name="eth1",instance="ip_qnap"} 0.0
network_output{id="3",if_name="eth2",instance="ip_qnap"} 0.0
network_output{id="4",if_name="eth3",instance="ip_qnap"} 0.0
network_output{id="5",if_name="eth4",instance="ip_qnap"} 1.455572796e+09
# HELP hdd_temperature HDD stack
# TYPE hdd_temperature gauge
hdd_temperature{id="1",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD1",state="GOOD"} 33.0
hdd_temperature{id="2",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD2",state="GOOD"} 34.0
hdd_temperature{id="3",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD3",state="GOOD"} 32.0
hdd_temperature{id="4",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD4",state="GOOD"} 32.0
# HELP hdd_capacity HDD stack
# TYPE hdd_capacity gauge
hdd_capacity{id="1",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD1",state="GOOD"} 4.000787030016e+012
hdd_capacity{id="2",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD2",state="GOOD"} 4.000787030016e+012
hdd_capacity{id="3",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD3",state="GOOD"} 4.000787030016e+012
hdd_capacity{id="4",instance="ip_qnap",model="WD4003FFBX-68MU3N0",name="HDD4",state="GOOD"} 4.000787030016e+012
# HELP system_fan_speed FAN stack
# TYPE system_fan_speed gauge
system_fan_speed{id="1",instance="ip_qnap",name="System FAN 1"} 6224.0
system_fan_speed{id="2",instance="ip_qnap",name="System FAN 2"} 6122.0
# HELP memory_total Memory stack
# TYPE memory_total gauge
memory_total{instance="ip_qnap"} 4.126560256e+09
# HELP memory_free Memory stack
# TYPE memory_free gauge
memory_free{instance="ip_qnap"} 3.284451328e+09
# HELP cpu_temperature CPU temperature
# TYPE cpu_temperature gauge
cpu_temperature{instance="ip_qnap"} 56.0
# HELP system_temperature System Temperature
# TYPE system_temperature gauge
system_temperature{instance="ip_qnap"} 47.0
# HELP system_name System name
# TYPE system_name gauge
system_name{instance="ip_qnap",systemname="QNAP-DS2\n"} 1.0
# HELP model_name Model name
# TYPE model_name gauge
model_name{instance="ip_qnap",modelname="TS-463U-RP"} 1.0

Отлично, все что остается это настроить Dashboard в Grafana, дело это конечно не быстрое и кропотливое, но результат того стоит.

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

Git: https://git.yakunin.dev/yakunin/python/-/tree/main/qnap_snmp_exporter

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *