feat: Zabbix 6.4 UserParameter template + fix scenario metrics

- Add zbx_ses_template_up.xml: Zabbix 6.4 template for UserParameter mode
  (1 master item + 11 dependent items with JSONPath preprocessing)
- Add write_agentd_config() to UserParameterWriter: atomically writes
  zbx_agentd_ses.conf on startup
- Call write_agentd_config() in main.py start() for userparameter mode
- Fix _run_due_scenarios: always write scenario metrics (even when
  disabled/empty) to prevent Zabbix JSONPath preprocessing failures
- Fill placeholder zbx_ses_monitor.conf with actual UserParameter line
- Update README: Zabbix 6/5 template table, Include instructions
This commit is contained in:
Maksim Totmin 2026-06-23 17:41:27 +07:00
parent 0f005d46f0
commit 32c59a4005
5 changed files with 423 additions and 46 deletions

View File

@ -55,14 +55,18 @@ Zabbix агент по запросу сервера запускает скри
и зависимые элементы с препроцессингом JSONPath. и зависимые элементы с препроцессингом JSONPath.
Настройка: Настройка:
1. Импортировать шаблон `zabbix/zbx_ses_template_5.0_up.xml` в Zabbix 1. Импортировать шаблон в Zabbix:
- Zabbix 6.4: `zabbix/zbx_ses_template_up.xml`
- Zabbix 5.0: `zabbix/zbx_ses_template_5.0_up.xml`
2. Привязать шаблон к хосту (Configuration → Hosts → выберите хост → Templates) 2. Привязать шаблон к хосту (Configuration → Hosts → выберите хост → Templates)
3. В конфиге монитора `config/config.yaml` установить `mode: "userparameter"` 3. В конфиге монитора `config/config.yaml` установить `mode: "userparameter"`
4. В `/etc/zabbix/zabbix_agentd.conf` добавить строку: 4. При старте демон автоматически создаёт конфиг агента:
`/opt/ses-monitor/config/zbx_agentd_ses.conf`
5. В `/etc/zabbix/zabbix_agentd.conf` добавить строку (или скопировать конфиг в `/etc/zabbix/zabbix_agentd.d/`):
``` ```
Include=/opt/ses-monitor/config/zbx_agentd_ses.conf Include=/opt/ses-monitor/config/zbx_agentd_ses.conf
``` ```
5. Перезапустить Zabbix агента: `sudo systemctl restart zabbix-agent` 6. Перезапустить Zabbix агента: `sudo systemctl restart zabbix-agent`
## Структура проекта ## Структура проекта
@ -86,9 +90,11 @@ Zabbix агент по запросу сервера запускает скри
│ ├── example_liveness.yaml # Пример: быстрая проверка │ ├── example_liveness.yaml # Пример: быстрая проверка
│ └── example_dialog.yaml # Пример: многошаговый диалог │ └── example_dialog.yaml # Пример: многошаговый диалог
├── zabbix/ ├── zabbix/
│ ├── zbx_ses_template_5.0.xml # Шаблон для sender (trapper) │ ├── zbx_ses_template.xml # Шаблон Zabbix 6.4 для sender (trapper)
│ ├── zbx_ses_template_5.0_up.xml # Шаблон для UserParameter │ ├── zbx_ses_template_up.xml # Шаблон Zabbix 6.4 для UserParameter
│ └── zbx_ses_monitor.conf # UserParameter для Zabbix агента │ ├── zbx_ses_template_5.0.xml # Шаблон Zabbix 5.0 для sender (trapper)
│ ├── zbx_ses_template_5.0_up.xml # Шаблон Zabbix 5.0 для UserParameter
│ └── zbx_ses_monitor.conf # Резервный конфиг UserParameter
├── scripts/ ├── scripts/
│ └── zbx_get.py # Скрипт чтения JSON для UserParameter │ └── zbx_get.py # Скрипт чтения JSON для UserParameter
├── systemd/ ├── systemd/
@ -181,8 +187,15 @@ journalctl -u ses-monitor -f
### 6. Импорт шаблона Zabbix ### 6. Импорт шаблона Zabbix
В зависимости от версии Zabbix и режима доставки:
| Zabbix | sender (trapper) | userparameter |
|---|---|---|
| 6.4 | `zabbix/zbx_ses_template.xml` | `zabbix/zbx_ses_template_up.xml` |
| 5.0 | `zabbix/zbx_ses_template_5.0.xml` | `zabbix/zbx_ses_template_5.0_up.xml` |
1. Zabbix Frontend → **Configuration → Templates → Import** 1. Zabbix Frontend → **Configuration → Templates → Import**
2. Выбрать файл `zabbix/zbx_ses_template_5.0.xml` (для sender) или `zabbix/zbx_ses_template_5.0_up.xml` (для UserParameter) 2. Выбрать файл шаблона
3. Нажать Import 3. Нажать Import
4. Привязать шаблон "SES Monitor" к нужному хосту 4. Привязать шаблон "SES Monitor" к нужному хосту

View File

@ -93,6 +93,7 @@ class SESMonitorApp:
self._config.ses.base_url, self._config.ses.base_url,
zbx_cfg.metrics_file, zbx_cfg.metrics_file,
) )
self._zabbix.write_agentd_config()
else: else:
log.info( log.info(
"SES Monitor v%s запускается (SES: %s, Zabbix: %s:%d, хост: %s)", "SES Monitor v%s запускается (SES: %s, Zabbix: %s:%d, хост: %s)",
@ -240,46 +241,46 @@ class SESMonitorApp:
Zabbix-метрик (ses.scenario.ok, .total, .failed, .avg_time, .max_time, Zabbix-метрик (ses.scenario.ok, .total, .failed, .avg_time, .max_time,
.error). Это делает шаблон Zabbix универсальным не нужно добавлять .error). Это делает шаблон Zabbix универсальным не нужно добавлять
отдельный item под каждый сценарий. отдельный item под каждый сценарий.
"""
if not self._scenario_runner:
return
Всегда пишет дефолтные значения метрик, даже если сценарии
отключены или не настроены иначе Zabbix-препроцессинг JSONPath
упадёт на отсутствующих ключах.
"""
due_results: List[ScenarioResult] = [] due_results: List[ScenarioResult] = []
for scenario in self._scenarios: if self._scenario_runner:
name = scenario.name or "unknown" for scenario in self._scenarios:
last_run = self._last_scenario_run.get(name, 0) name = scenario.name or "unknown"
last_run = self._last_scenario_run.get(name, 0)
if now - last_run >= scenario.interval: if now - last_run >= scenario.interval:
try: try:
result = await self._scenario_runner.run_one( result = await self._scenario_runner.run_one(
self._client, scenario self._client, scenario
)
due_results.append(result)
self._last_scenario_run[name] = now
if not result.passed:
log.warning(
"Сценарий '%s': ПРОВАЛЕН за %dms (ошибок: %d шагов)",
name,
round(result.total_time_ms),
len(result.failed_steps),
) )
except Exception as e: due_results.append(result)
log.exception( self._last_scenario_run[name] = now
"Сценарий '%s': неожиданное исключение: %s", name, e
)
# Агрегируем результаты и отправляем фиксированные ключи if not result.passed:
if due_results and self._zabbix.enabled: log.warning(
"Сценарий '%s': ПРОВАЛЕН за %dms (ошибок: %d шагов)",
name,
round(result.total_time_ms),
len(result.failed_steps),
)
except Exception as e:
log.exception(
"Сценарий '%s': неожиданное исключение: %s", name, e
)
if self._zabbix.enabled:
agg = aggregate_scenario_results(due_results) agg = aggregate_scenario_results(due_results)
self._zabbix.add("ses.scenario.ok", agg["ok"]) self._zabbix.add("ses.scenario.ok", agg["ok"])
self._zabbix.add("ses.scenario.total", agg["total"]) self._zabbix.add("ses.scenario.total", agg["total"])
self._zabbix.add("ses.scenario.failed", agg["failed"]) self._zabbix.add("ses.scenario.failed", agg["failed"])
self._zabbix.add("ses.scenario.avg_time", agg["avg_time"]) self._zabbix.add("ses.scenario.avg_time", agg["avg_time"])
self._zabbix.add("ses.scenario.max_time", agg["max_time"]) self._zabbix.add("ses.scenario.max_time", agg["max_time"])
if agg["error"]: self._zabbix.add("ses.scenario.error", agg["error"])
self._zabbix.add("ses.scenario.error", agg["error"])
async def _run_log_check(self) -> None: async def _run_log_check(self) -> None:
"""Выполняет проверку логов (если включено).""" """Выполняет проверку логов (если включено)."""

View File

@ -220,6 +220,43 @@ class UserParameterWriter:
""" """
return ( return (
"# Автоматически сгенерировано ses-monitor.\n" "# Автоматически сгенерировано ses-monitor.\n"
"# Требуется: Include=/opt/ses-monitor/config/zbx_agentd_ses.conf\n" "# Добавьте в /etc/zabbix/zabbix_agentd.conf:\n"
f"# Include={self._config.agentd_config_dir}/zbx_agentd_ses.conf\n"
"# Или скопируйте этот файл в /etc/zabbix/zabbix_agentd.d/\n"
f"UserParameter=ses.metrics,/opt/ses-monitor/scripts/zbx_get.py\n" f"UserParameter=ses.metrics,/opt/ses-monitor/scripts/zbx_get.py\n"
) )
def write_agentd_config(self) -> bool:
"""Записывает конфигурационный файл UserParameter на диск.
Создаёт или обновляет zbx_agentd_ses.conf в agentd_config_dir.
Использует атомарную запись (temp + rename).
Returns:
True если запись успешна, False при ошибке.
"""
config_dir = Path(self._config.agentd_config_dir)
config_path = config_dir / "zbx_agentd_ses.conf"
content = self.generate_agentd_config()
try:
config_dir.mkdir(parents=True, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
dir=str(config_dir),
prefix=".zbx_agentd_",
suffix=".tmp",
)
try:
os.write(fd, content.encode("utf-8"))
os.fsync(fd)
os.close(fd)
os.replace(tmp_path, str(config_path))
os.chmod(str(config_path), 0o644)
except Exception:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
raise
log.info("Конфиг Zabbix агента записан: %s", config_path)
return True
except Exception as e:
log.error("Ошибка записи конфига Zabbix агента: %s", e)
return False

View File

@ -1,10 +1,5 @@
# ============================================================================= # Автоматически сгенерировано ses-monitor.
# Резервный вариант: UserParameter для Zabbix Agent (пассивный режим). # Добавьте в /etc/zabbix/zabbix_agentd.conf:
# ============================================================================= # Include=/opt/ses-monitor/config/zbx_agentd_ses.conf
# Используется если zabbix_sender недоступен. # Или скопируйте этот файл в /etc/zabbix/zabbix_agentd.d/
# Поместить в /etc/zabbix/zabbix_agentd.d/ses_monitor.conf UserParameter=ses.metrics,/opt/ses-monitor/scripts/zbx_get.py
# и перезапустить zabbix-agent: systemctl restart zabbix-agent
#
# Скрипт ses_monitor_zabbix_agent.py должен быть установлен в
# /opt/ses-monitor/scripts/ses_monitor_zabbix_agent.py
# =============================================================================

View File

@ -0,0 +1,331 @@
<?xml version="1.0" encoding="UTF-8"?>
<zabbix_export>
<version>6.4</version>
<template_groups>
<template_group>
<uuid>88166f35-c76b-44e2-b8ad-333b3b4b9692</uuid>
<name>Templates</name>
</template_group>
</template_groups>
<templates>
<template>
<uuid>f4e7d78a-8163-46db-a7cf-e9a79df9a740</uuid>
<template>SES Monitor</template>
<name>SES Monitor — Service Engine Server</name>
<description>Шаблон для мониторинга диалоговой сценарной машины SES.
Метрики передаются watchdog-демоном ses-monitor через UserParameter (Zabbix агент).
JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json и читается
скриптом scripts/zbx_get.py. Master-элемент: ses.metrics.
Проверяет: доступность API, лицензию, выполнение сценариев.
Сценарии задаются в YAML-файлах (config/scenarios/). Все сценарии
агрегируются в фиксированные метрики — шаблон универсален и не зависит
от количества или состава сценариев.</description>
<groups>
<group>
<name>Templates</name>
</group>
</groups>
<macros>
<macro>
<macro>{$SES_API_TIMEOUT_MS}</macro>
<value>5000</value>
<description>Порог медленного ответа API (миллисекунды)</description>
</macro>
</macros>
<items>
<item>
<uuid>b5d05b4d-7b9d-4e63-98de-7fe507748eaa</uuid>
<name>SES: все метрики (JSON)</name>
<type>ZABBIX_PASSIVE</type>
<key>ses.metrics</key>
<delay>30s</delay>
<history>7d</history>
<trends>0</trends>
<value_type>TEXT</value_type>
<description>Полный JSON со всеми метриками ses-monitor. Используется как master item для dependent items с JSONPath.</description>
</item>
<!-- Liveness -->
<item>
<uuid>b6a64924-6ecf-406a-80e1-cb5ad8f4d9e4</uuid>
<name>SES: API доступность</name>
<type>DEPENDENT</type>
<key>ses.api.liveness</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.api.liveness"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>0 — API недоступен, 1 — работает. Проверяется GET /ses/hostname.</description>
<triggers>
<trigger>
<uuid>b5798e51-dc19-4d90-aa68-45107a7945e5</uuid>
<expression>last(/SES Monitor/ses.api.liveness)=0</expression>
<name>SES API недоступен на {HOST.NAME}</name>
<priority>HIGH</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>ce800e89-14a8-4084-bd16-0abd594b8ef4</uuid>
<name>SES: время ответа API</name>
<type>DEPENDENT</type>
<key>ses.api.response_time</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.api.response_time"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>ms</units>
<description>Время ответа GET /ses/hostname в миллисекундах.</description>
<triggers>
<trigger>
<uuid>6813663c-2e56-4b42-aecd-3dfd54d99ffb</uuid>
<expression>last(/SES Monitor/ses.api.response_time)&gt;{$SES_API_TIMEOUT_MS}</expression>
<name>SES: медленный ответ API (&gt;{$SES_API_TIMEOUT_MS} мс)</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>05fa180f-9e01-4fde-b73c-fa7b1d9522f8</uuid>
<name>SES: статус лицензии</name>
<type>DEPENDENT</type>
<key>ses.license.ok</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.license.ok"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>0 — проблема с лицензией, 1 — в порядке.</description>
<triggers>
<trigger>
<uuid>863d9b4b-8a9d-4e0c-9b9d-fd3c8022df82</uuid>
<expression>last(/SES Monitor/ses.license.ok)=0</expression>
<name>SES: проблема с лицензией на {HOST.NAME}</name>
<priority>HIGH</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>5c090eb8-6152-4fb5-a49a-ebf7d803f975</uuid>
<name>SES: дней до окончания лицензии</name>
<type>DEPENDENT</type>
<key>ses.license.days_remaining</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.license.days_remaining"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>d</units>
<description>Количество дней до окончания лицензии SES. -1 = бессрочная.</description>
</item>
<item>
<uuid>696ac7fe-11f3-42f9-a56e-c2533bc7d71c</uuid>
<name>SES: количество сценариев (роботов)</name>
<type>DEPENDENT</type>
<key>ses.robots.count</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.robots.count"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>Общее количество зарегистрированных сценариев (роботов).
Изменение значения может сигнализировать о проблемах конфигурации.</description>
</item>
<!-- Сценарии -->
<item>
<uuid>48930571-519a-4071-adec-fb22a732eaad</uuid>
<name>SES: тестовые сценарии — все успешны</name>
<type>DEPENDENT</type>
<key>ses.scenario.ok</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.scenario.ok"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>1 — все сценарии прошли, 0 — есть упавшие.</description>
<triggers>
<trigger>
<uuid>a44be8fd-d652-40a2-8be2-b9760df19975</uuid>
<expression>last(/SES Monitor/ses.scenario.ok)=0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>AVERAGE</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>b28ac087-8070-4195-95ea-99ccb24fa370</uuid>
<name>SES: тестовые сценарии — всего</name>
<type>DEPENDENT</type>
<key>ses.scenario.total</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.scenario.total"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>Количество активных сценариев мониторинга.</description>
</item>
<item>
<uuid>5b003d5f-f88b-47e3-a3dc-17d196036351</uuid>
<name>SES: тестовые сценарии — упало</name>
<type>DEPENDENT</type>
<key>ses.scenario.failed</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.scenario.failed"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>Количество упавших сценариев за последний цикл.</description>
<triggers>
<trigger>
<uuid>4de7a731-b6b7-41c6-85a2-71a5f1a45a66</uuid>
<expression>last(/SES Monitor/ses.scenario.failed)&gt;0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>0f911f5a-7529-495d-b35a-d1784a32a0de</uuid>
<name>SES: тестовые сценарии — среднее время</name>
<type>DEPENDENT</type>
<key>ses.scenario.avg_time</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.scenario.avg_time"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>ms</units>
<description>Среднее время выполнения сценария (миллисекунды).</description>
</item>
<item>
<uuid>5747ec81-622c-44dc-8eca-c9be38458dad</uuid>
<name>SES: тестовые сценарии — макс. время</name>
<type>DEPENDENT</type>
<key>ses.scenario.max_time</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.scenario.max_time"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>ms</units>
<description>Максимальное время выполнения сценария (миллисекунды).</description>
</item>
<item>
<uuid>bafb3afc-2ac4-4608-9f5b-d49d88960921</uuid>
<name>SES: тестовые сценарии — ошибка</name>
<type>DEPENDENT</type>
<key>ses.scenario.error</key>
<master_item>
<key>ses.metrics</key>
</master_item>
<preprocessing>
<step>
<type>JSONPATH</type>
<params>$["ses.scenario.error"]</params>
</step>
</preprocessing>
<history>7d</history>
<trends>0</trends>
<value_type>TEXT</value_type>
<description>Текст ошибки: "N сценариев упали". Пустая строка если всё хорошо.</description>
</item>
</items>
</template>
</templates>
</zabbix_export>