Универсальный watchdog-мониторинг Service Engine Server (SES): - Liveness-проверки (API, лицензия, роботы) - Диалоговые сценарии из YAML с агрегацией в 6 фиксированных Zabbix-метрик - Доставка: zabbix_sender (active push) или UserParameter (пассивный опрос) - Поддержка Python 3.7+, Zabbix 5.0
226 lines
7.7 KiB
Python
226 lines
7.7 KiB
Python
"""
|
||
Отправка метрик в Zabbix.
|
||
|
||
Два режима:
|
||
- sender: zabbix_sender (активный push).
|
||
- userparameter: запись JSON-файла + конфиг для Zabbix агента (пассивный опрос).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import concurrent.futures
|
||
import json
|
||
import logging
|
||
import os
|
||
import subprocess
|
||
import tempfile
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from ses_monitor.config import ZabbixConfig, ZabbixMode
|
||
|
||
log = logging.getLogger("ses_monitor.zabbix")
|
||
|
||
|
||
@dataclass
|
||
class ZabbixMetric:
|
||
"""Одна метрика для Zabbix."""
|
||
|
||
host: str
|
||
key: str
|
||
value: str
|
||
|
||
def to_line(self) -> str:
|
||
"""Форматирует метрику в строку для zabbix_sender."""
|
||
return f"{self.host} {self.key} {self.value}"
|
||
|
||
|
||
class ZabbixSender:
|
||
"""Асинхронная обёртка над zabbix_sender.
|
||
|
||
Отправляет метрики через stdin утилиты zabbix_sender.
|
||
Использует run_in_executor + subprocess.run (синхронный), чтобы избежать
|
||
проблем с asyncio child watcher на Python 3.7.
|
||
"""
|
||
|
||
def __init__(self, config: ZabbixConfig):
|
||
self._config = config
|
||
self._metrics: List[ZabbixMetric] = []
|
||
self._executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
||
|
||
@property
|
||
def enabled(self) -> bool:
|
||
"""Включена ли отправка в Zabbix."""
|
||
return self._config.enabled
|
||
|
||
def add(self, key: str, value: Any) -> None:
|
||
"""Добавляет метрику в буфер отправки.
|
||
|
||
Args:
|
||
key: Ключ метрики (например, "ses.api.liveness").
|
||
value: Значение. Числа и булевы конвертируются в строку.
|
||
"""
|
||
str_value = str(value) if not isinstance(value, bool) else str(int(value))
|
||
metric = ZabbixMetric(
|
||
host=self._config.host,
|
||
key=key,
|
||
value=str_value,
|
||
)
|
||
self._metrics.append(metric)
|
||
|
||
def _run_sender(self, input_data: str) -> bool:
|
||
"""Синхронный запуск zabbix_sender (выполняется в thread pool)."""
|
||
try:
|
||
proc = subprocess.run(
|
||
[
|
||
self._config.sender_path,
|
||
"-z", self._config.server,
|
||
"-p", str(self._config.port),
|
||
"-s", self._config.host,
|
||
"-i", "-",
|
||
],
|
||
input=input_data.encode("utf-8"),
|
||
capture_output=True,
|
||
timeout=30,
|
||
)
|
||
|
||
if proc.returncode != 0:
|
||
log.error(
|
||
"zabbix_sender завершился с кодом %d: %s",
|
||
proc.returncode,
|
||
proc.stderr.decode("utf-8", errors="replace").strip(),
|
||
)
|
||
return False
|
||
|
||
output = proc.stdout.decode("utf-8", errors="replace")
|
||
log.debug(
|
||
"Zabbix: отправлено метрик. Ответ: %s",
|
||
output.strip(),
|
||
)
|
||
return True
|
||
|
||
except FileNotFoundError:
|
||
log.error(
|
||
"zabbix_sender не найден по пути %s. Установите пакет.",
|
||
self._config.sender_path,
|
||
)
|
||
return False
|
||
except subprocess.TimeoutExpired:
|
||
log.error("zabbix_sender: таймаут отправки (30с)")
|
||
return False
|
||
except Exception as e:
|
||
log.error("zabbix_sender: ошибка отправки: %s", e)
|
||
return False
|
||
|
||
async def flush(self) -> bool:
|
||
"""Отправляет накопленные метрики в Zabbix.
|
||
|
||
Returns:
|
||
True если отправка успешна, False при ошибке.
|
||
"""
|
||
if not self._metrics:
|
||
return True
|
||
|
||
if not self._config.enabled:
|
||
self._metrics.clear()
|
||
return True
|
||
|
||
lines = "\n".join(m.to_line() for m in self._metrics) + "\n"
|
||
self._metrics.clear()
|
||
|
||
loop = asyncio.get_running_loop()
|
||
return await loop.run_in_executor(self._executor, self._run_sender, lines)
|
||
|
||
|
||
class UserParameterWriter:
|
||
"""Запись метрик в JSON для Zabbix UserParameter.
|
||
|
||
Пишет атомарный JSON во временный файл с последующей заменой.
|
||
Zabbix агент читает файл через UserParameter-скрипт zbx_get.py.
|
||
"""
|
||
|
||
def __init__(self, config: ZabbixConfig):
|
||
self._config = config
|
||
self._metrics: Dict[str, str] = {}
|
||
self._executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
||
|
||
@property
|
||
def enabled(self) -> bool:
|
||
"""Включена ли запись метрик."""
|
||
return self._config.enabled
|
||
|
||
def add(self, key: str, value: Any) -> None:
|
||
"""Добавляет метрику в буфер.
|
||
|
||
Args:
|
||
key: Ключ метрики (например, "ses.api.liveness").
|
||
value: Значение. Конвертируется в строку.
|
||
"""
|
||
str_value = str(value) if not isinstance(value, bool) else str(int(value))
|
||
self._metrics[key] = str_value
|
||
|
||
async def flush(self) -> bool:
|
||
"""Сбрасывает буфер метрик в JSON-файл и обновляет конфиг агента.
|
||
|
||
Returns:
|
||
True если запись успешна, False при ошибке.
|
||
"""
|
||
if not self._metrics:
|
||
return True
|
||
|
||
if not self._config.enabled:
|
||
self._metrics.clear()
|
||
return True
|
||
|
||
data = dict(self._metrics)
|
||
self._metrics.clear()
|
||
|
||
loop = asyncio.get_running_loop()
|
||
ok = await loop.run_in_executor(
|
||
self._executor, self._write_json, data
|
||
)
|
||
return ok
|
||
|
||
def _write_json(self, data: Dict[str, str]) -> bool:
|
||
"""Атомарная запись JSON-файла с метриками."""
|
||
metrics_path = Path(self._config.metrics_file)
|
||
try:
|
||
metrics_path.parent.mkdir(parents=True, exist_ok=True)
|
||
tmp = tempfile.NamedTemporaryFile(
|
||
mode="w",
|
||
encoding="utf-8",
|
||
dir=str(metrics_path.parent),
|
||
prefix=".metrics_",
|
||
suffix=".tmp",
|
||
delete=False,
|
||
)
|
||
tmp_path = tmp.name
|
||
try:
|
||
json.dump(data, tmp, ensure_ascii=False, indent=2)
|
||
tmp.write("\n")
|
||
tmp.close()
|
||
os.replace(tmp_path, str(metrics_path))
|
||
os.chmod(str(metrics_path), 0o644)
|
||
except Exception:
|
||
if os.path.exists(tmp_path):
|
||
os.unlink(tmp_path)
|
||
raise
|
||
return True
|
||
except Exception as e:
|
||
log.error("Ошибка записи JSON-файла метрик: %s", e)
|
||
return False
|
||
|
||
def generate_agentd_config(self) -> str:
|
||
"""Генерирует содержимое конфигурационного файла UserParameter.
|
||
|
||
Returns:
|
||
Строка с конфигурацией для Zabbix агента.
|
||
"""
|
||
return (
|
||
"# Автоматически сгенерировано ses-monitor.\n"
|
||
"# Требуется: Include=/opt/ses-monitor/config/zbx_agentd_ses.conf\n"
|
||
f"UserParameter=ses.metrics,/opt/ses-monitor/scripts/zbx_get.py\n"
|
||
)
|