Compare commits

..
12 Commits
Author SHA1 Message Date
Maksim Totmin dc8d4db358 refactor: remove dead ArchiveCheckConfig and stale archive_task docs
- Remove ArchiveCheckConfig dataclass and archive_check field (never used)
- Remove archive parsing from _dict_to_scenario_config()
- Fix main.py docstring: archive_task never existed in _main_loop()
2026-06-24 13:39:57 +07:00
Maksim Totmin a5e17bcdd8 docs: move build instructions to cli/README.md, keep main README lean 2026-06-24 11:17:41 +07:00
Maksim Totmin 28f5a407c9 feat: add binary build instructions and __main__ guard for pyinstaller
- Add __main__ guard to cli.py for pyinstaller packaging
- Document clean-venv build producing ~11 MB binary
- Add *.spec to .gitignore
- Exclude heavy stdlib modules in build command
2026-06-24 11:14:32 +07:00
Maksim Totmin 9feb14a33a docs: add cli.py and cli/ to project structure 2026-06-24 11:04:41 +07:00
Maksim Totmin c066ae687a feat: separate ses-test CLI install via subdirectory
Add cli/pyproject.toml so analysts can install only ses-test
without the monitoring daemon or Zabbix integration.

pip install "ses-test @ git+https://...#subdirectory=cli"
→ only ses-test binary, no ses-monitor in PATH

Root pyproject.toml unchanged — daemon install still provides both.

docs: rename section to 'Установка ses-test'
2026-06-24 10:50:47 +07:00
Maksim Totmin fbfd073369 feat: add ses-test CLI for interactive YAML scenario testing
- New ses-test command: run a single YAML scenario against real SES
  step-by-step with persistent session tracking
- --verbose shows raw JSON responses, --dry-run validates YAML only
- --session / --robot-id overrides for iterative debugging
- Add verify param to SESClient for self-signed HTTPS support
- Add load_scenario_file() to config.py for single-file loading
- Update README with ses-test usage examples and output samples
2026-06-24 10:37:41 +07:00
Maksim Totmin fd45734a74 fix Zabbix 6.0 template triggers: avg(#3) on scenario.failed, remove duplicate ok trigger
- Change ses.scenario.failed triggers: last() -> avg(#3) for reliable detection
- Remove ses.scenario.ok trigger (duplicates scenario.failed > 0)
- Fix preprocessing: parameters with parameter child elements for Zabbix 6.0
- Remove UUID dashes (32 hex chars) for Zabbix 6.0 compatibility
- Fix template_groups -> groups format for Zabbix 6.0
- Fix strlen() -> length() for Zabbix 6.0 trigger compatibility
- All templates now have 6 triggers each
2026-06-23 19:24:03 +07:00
Maksim Totmin cc53577e31 fix parameters tag: wrap value in <parameter> element for Zabbix 5.0/6.0 compatibility 2026-06-23 18:18:29 +07:00
Maksim Totmin b3b18b019b fix preprocessing step tag for Zabbix 5.0/6.0 compatibility: params -> parameters 2026-06-23 18:17:01 +07:00
Maksim Totmin 11a96222f4 add triggers to all Zabbix templates
- add WARNING trigger on ses.license.days_remaining (< 30 days)
- add CRITICAL trigger on ses.scenario.error (strlen > 0)
- populate all 5 missing triggers in zbx_ses_template_5.0_up.xml

All 4 templates now have 7 triggers each.
2026-06-23 18:13:43 +07:00
Maksim Totmin 32c59a4005 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
2026-06-23 17:41:27 +07:00
Maksim Totmin 0f005d46f0 fix: correct venv path in systemd unit (add missing dot before venv) 2026-06-23 17:19:25 +07:00
16 changed files with 1107 additions and 105 deletions
+1
View File
@@ -5,6 +5,7 @@ __pycache__/
*.egg *.egg
dist/ dist/
build/ build/
*.spec
# --- Virtual environment --- # --- Virtual environment ---
.venv/ .venv/
+126 -11
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`
## Структура проекта ## Структура проекта
@@ -79,6 +83,7 @@ Zabbix агент по запросу сервера запускает скри
│ ├── scenario_runner.py # Движок выполнения сценариев │ ├── scenario_runner.py # Движок выполнения сценариев
│ ├── log_client.py # Чтение логов SES (задел на будущее) │ ├── log_client.py # Чтение логов SES (задел на будущее)
│ ├── zabbix.py # Интеграция с Zabbix (sender / UserParameter) │ ├── zabbix.py # Интеграция с Zabbix (sender / UserParameter)
│ ├── cli.py # CLI ses-test: тестирование YAML-сценариев
│ └── utils.py # Утилиты (логирование, JSON-пути, проверки) │ └── utils.py # Утилиты (логирование, JSON-пути, проверки)
├── config/ ├── config/
│ ├── config.yaml.example # Шаблон конфигурации (скопировать в config.yaml) │ ├── config.yaml.example # Шаблон конфигурации (скопировать в config.yaml)
@@ -86,9 +91,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/
@@ -96,6 +103,9 @@ Zabbix агент по запросу сервера запускает скри
├── tests/ ├── tests/
│ ├── __init__.py │ ├── __init__.py
│ └── test_scenario_runner.py # 48 тестов │ └── test_scenario_runner.py # 48 тестов
├── cli/
│ ├── pyproject.toml # Отдельная сборка: только ses-test
│ └── README.md # Инструкции: установка, сборка бинарника
├── pyproject.toml # Зависимости и метаданные пакета ├── pyproject.toml # Зависимости и метаданные пакета
└── README.md └── README.md
``` ```
@@ -181,11 +191,38 @@ 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" к нужному хосту
## Установка ses-test
Если нужна только утилита `ses-test` для отладки YAML-сценариев (без демона и Zabbix):
```bash
pip install "ses-test @ git+https://git.totmin.ru/en2zmax/ses-monitor.git#subdirectory=cli"
```
После установки доступна одна команда — `ses-test`:
```bash
ses-test --help
ses-test my_scenario.yaml --ses-url http://ses-host:6190
ses-test my_scenario.yaml --ses-url http://ses-host:6190 --dry-run
```
Подробнее — см. раздел [«Как адаптировать под ваш проект»](#как-адаптировать-под-ваш-проект).
Сборка в исполняемый файл — см. [cli/README.md](cli/README.md).
## Формат сценариев ## Формат сценариев
Сценарии описываются в YAML-файлах в каталоге `config/scenarios/`. Это гибкий DSL, который не зависит от конкретного проекта SES. Сценарии описываются в YAML-файлах в каталоге `config/scenarios/`. Это гибкий DSL, который не зависит от конкретного проекта SES.
@@ -273,15 +310,93 @@ path: "request.data[0].reply.confidence"
3. **Создайте файл сценария**, описав шаги диалога: 3. **Создайте файл сценария**, описав шаги диалога:
```bash ```bash
cp config/scenarios/example_liveness.yaml config/scenarios/$(hostname).yaml cp config/scenarios/example_dialog.yaml config/scenarios/my_project.yaml
vim config/scenarios/$(hostname).yaml vim config/scenarios/my_project.yaml
``` ```
4. **Протестируйте вручную** шаги через curl: 4. **Протестируйте сценарий** утилитой `ses-test`:
```bash ```bash
curl -X POST "http://<ses-host>:6190/ses/ask/<robot_id>?text=Привет&channel=monitoring" # Базовый прогон
ses-test config/scenarios/my_project.yaml --ses-url http://ses-host:6190
# С детальным выводом JSON-ответов (режим отладки)
ses-test config/scenarios/my_project.yaml --ses-url http://ses-host:6190 -v
# Только валидация YAML без запросов к SES
ses-test config/scenarios/my_project.yaml --ses-url http://ses-host:6190 --dry-run
# С переопределением robot_id (если не указан в YAML)
ses-test config/scenarios/my_project.yaml --ses-url http://ses-host:6190 --robot-id abc-123
# С фиксированной сессией для повторных прогонов
ses-test config/scenarios/my_project.yaml --ses-url http://ses-host:6190 --session debug-1
``` ```
**Примеры вывода:**
Успешный прогон (без `-v`):
```
=== Сценарий: 'test: МФЦ диалог' ===
SES: http://ses-host:6190
Robot ID: abc-123-def-456
Канал: monitoring
Шагов: 2
Сессия: monitoring_test_1782271424
── Шаг 1/2 ────────────────────────────────────────
→ Привет
← Время ответа: 176ms
[PASS] path=error equals=0
[PASS] path=answer[0].messages[0] not_empty
── Шаг 2/2 ────────────────────────────────────────
→ Записаться на приём
← Время ответа: 382ms
[PASS] path=error equals=0
[PASS] path=answer[0].messages[0] not_empty
──────────────────────────────────────────────────
РЕЗУЛЬТАТ: УСПЕШНО (2/2 шагов)
```
Проваленный шаг:
```
── Шаг 1/1 ────────────────────────────────────────
→ Привет
← Время ответа: 177ms
[PASS] path=error equals=0
[FAIL] Поле 'answer[0].messages[0]' = 'пожалуйста выберите...'
не содержит 'несуществующая_фраза_xyz_999'
──────────────────────────────────────────────────
РЕЗУЛЬТАТ: ПРОВАЛЕН (шаг 1)
```
Режим отладки (`-v`):
```
── Шаг 1/2 ────────────────────────────────────────
→ Привет
← Время ответа: 176ms
← RAW JSON:
{
"error": 0,
"question": "Привет",
"answer": [{
"messages": [
"Пожалуйста выберите действие или задайте вопрос.\n\n{button:...}\n"
],
"voice": "Коля",
"interruptible": false
}],
"session": "monitoring_test_1782271424",
"uuid": "132e530c-..."
}
[PASS] path=error equals=0
[PASS] path=answer[0].messages[0] not_empty
```
**Exit code:** `0` — все шаги пройдены, `1` — есть ошибки, `2` — ошибка аргументов.
5. **Запустите демон** — метрики агрегируются автоматически (шаблон Zabbix универсален) 5. **Запустите демон** — метрики агрегируются автоматически (шаблон Zabbix универсален)
## Расширение системы ## Расширение системы
+51
View File
@@ -0,0 +1,51 @@
# ses-test
CLI-утилита для пошагового тестирования YAML-сценариев SES.
## Установка
```bash
pip install "ses-test @ git+https://git.totmin.ru/en2zmax/ses-monitor.git#subdirectory=cli"
```
## Сборка исполняемого файла
Можно собрать `ses-test` в один бинарник (~11 MB), не требующий Python на целевой машине.
Сборка производится в чистом виртуальном окружении — чтобы в бинарник не попали лишние системные пакеты (numpy, pillow и др.):
```bash
# 1. Клонируем репозиторий
git clone https://git.totmin.ru/en2zmax/ses-monitor.git
cd ses-monitor
# 2. Чистое окружение
python -m venv /tmp/build
/tmp/build/bin/pip install ./cli pyinstaller
# 3. Сборка
/tmp/build/bin/pyinstaller --onefile --name ses-test \
--paths src/ \
--hidden-import ses_monitor.utils \
--strip \
--exclude-module tkinter \
--exclude-module turtle \
--exclude-module sqlite3 \
--exclude-module curses \
--exclude-module idlelib \
--exclude-module pydoc \
--exclude-module venv \
--exclude-module ensurepip \
--exclude-module unittest \
--exclude-module test \
--exclude-module multiprocessing \
src/ses_monitor/cli.py
```
Готовый бинарник: `dist/ses-test` (~11 MB, Linux) или `dist/ses-test.exe` (Windows — запустить те же команды на Windows).
Для сборки под другую ОС нужна эта ОС — PyInstaller не кросс-компилирует.
## Справка
Основная документация: [README.md](../README.md)
+34
View File
@@ -0,0 +1,34 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "ses-test"
version = "1.0.0"
description = "CLI для пошагового тестирования YAML-сценариев SES"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [
{name = "SES Monitoring Team"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Topic :: Software Development :: Testing",
]
dependencies = [
"httpx>=0.21",
"pyyaml>=5.4",
]
[project.scripts]
ses-test = "ses_monitor.cli:main"
[tool.setuptools.packages.find]
where = ["../src"]
include = ["ses_monitor*"]
+1
View File
@@ -36,6 +36,7 @@ dev = [
[project.scripts] [project.scripts]
ses-monitor = "ses_monitor.main:main" ses-monitor = "ses_monitor.main:main"
ses-test = "ses_monitor.cli:main"
[build-system] [build-system]
requires = ["setuptools>=61.0"] requires = ["setuptools>=61.0"]
+303
View File
@@ -0,0 +1,303 @@
"""
CLI-утилита для интерактивного тестирования YAML-сценариев SES.
Позволяет аналитикам прогонять сценарий пошагово против реального SES-сервера
и видеть результат каждой проверки expect — без запуска всего демона ses-monitor.
Usage:
ses-test scenario.yaml --ses-url http://ses-host:6190
ses-test scenario.yaml --ses-url http://ses-host:6190 --verbose
ses-test scenario.yaml --ses-url http://ses-host:6190 --dry-run
"""
from __future__ import annotations
import argparse
import asyncio
import json
import sys
import time
from typing import Any, Dict, List
from ses_monitor.config import SESConfig, load_scenario_file
from ses_monitor.session_client import SESClient
from ses_monitor.utils import timer
def _format_expectation_results(
expectations: List[Dict[str, Any]],
response: Dict[str, Any],
) -> List[str]:
"""Форматирует результаты проверок ожиданий для одного шага.
Возвращает список строк [PASS]/[FAIL] для каждого expect-условия.
"""
results: List[str] = []
for exp in expectations:
path = exp.get("path", "?")
for condition, expected in exp.items():
if condition == "path":
continue
# check_expectation импортируется динамически для компактности —
# используем check_all_expectations с одним expectation
from ses_monitor.utils import check_expectation
ok, msg = check_expectation(response, path, condition, expected)
if ok:
if condition in ("exists", "not_empty"):
results.append(f" [PASS] path={path} {condition}")
else:
results.append(f" [PASS] path={path} {condition}={expected!r}")
else:
results.append(f" [FAIL] {msg}")
return results
def _truncate(text: str, max_len: int = 120) -> str:
"""Обрезает длинную строку для вывода."""
if len(text) <= max_len:
return text
return text[:max_len] + ""
def parse_args(argv: List[str] | None = None) -> argparse.Namespace:
"""Разбирает аргументы командной строки."""
parser = argparse.ArgumentParser(
prog="ses-test",
description="Пошаговое тестирование YAML-сценария SES",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""\
Примеры:
ses-test my_scenario.yaml --ses-url http://ses:6190
ses-test my_scenario.yaml --ses-url http://ses:6190 -v
ses-test my_scenario.yaml --ses-url http://ses:6190 --robot-id abc-123
ses-test my_scenario.yaml --ses-url http://ses:6190 --dry-run
ses-test my_scenario.yaml --ses-url https://ses:443 --no-verify
ses-test my_scenario.yaml --ses-url http://ses:6190 --session debug-1 -v
""",
)
parser.add_argument(
"scenario",
help="Путь к YAML-файлу сценария",
)
parser.add_argument(
"--ses-url",
required=True,
metavar="URL",
help="URL SES API (например, http://ses-host:6190)",
)
parser.add_argument(
"--robot-id",
metavar="ID",
help="Переопределить robot_id из YAML",
)
parser.add_argument(
"--channel",
metavar="CH",
default="monitoring",
help="Канал диалога (по умолчанию: monitoring)",
)
parser.add_argument(
"--timeout",
type=int,
metavar="SEC",
help="Таймаут HTTP-запроса, секунды (по умолчанию: из сценария или 30)",
)
parser.add_argument(
"--session",
metavar="ID",
help="ID сессии для повторных прогонов (по умолчанию: авто-генерация)",
)
parser.add_argument(
"--restart-closed",
action="store_true",
help="Переоткрывать закрытую сессию (restartClosed=1)",
)
parser.add_argument(
"--no-verify",
action="store_true",
help="Не проверять SSL-сертификат (для self-signed HTTPS)",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Выводить полный JSON-ответ SES для каждого шага",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Только валидация YAML, без запросов к SES",
)
return parser.parse_args(argv)
def main(argv: List[str] | None = None) -> int:
"""Точка входа CLI-утилиты ses-test.
Returns:
0 — все шаги пройдены.
1 — сценарий провален или ошибка выполнения.
2 — ошибка аргументов или конфигурации.
"""
args = parse_args(argv)
# Принудительный UTF-8 для Windows-консоли
if sys.platform == "win32":
sys.stdout.reconfigure(encoding="utf-8")
# Загрузка сценария
try:
scenario = load_scenario_file(args.scenario)
except FileNotFoundError as e:
print(f"ОШИБКА: {e}", file=sys.stderr)
return 2
except RuntimeError as e:
print(f"ОШИБКА: {e}", file=sys.stderr)
return 2
# Применяем CLI-переопределения поверх YAML
if args.robot_id:
scenario.robot_id = args.robot_id
if args.channel:
scenario.channel = args.channel
if args.timeout:
scenario.timeout = args.timeout
if args.restart_closed:
scenario.restart_closed = True
# Проверка обязательных полей
if not scenario.robot_id:
print(
"ОШИБКА: не указан robot_id.\n"
" Добавьте 'robot_id: \"uuid-...\"' в YAML-сценарий\n"
" или передайте через --robot-id.\n"
" Узнать ID: GET http://<ses-host>:6190/ses/robot/listall",
file=sys.stderr,
)
return 2
if not scenario.steps:
print(
f"ПРЕДУПРЕЖДЕНИЕ: сценарий '{scenario.name}' не содержит шагов.",
file=sys.stderr,
)
# Dry-run: только валидация, без запросов
if args.dry_run:
total_expects = sum(len(s.expect) for s in scenario.steps)
print(f"Сценарий '{scenario.name}' валиден.")
print(f" Шагов: {len(scenario.steps)}")
print(f" Ожиданий: {total_expects}")
print(f" Robot ID: {scenario.robot_id}")
print(f" Канал: {scenario.channel or 'monitoring'}")
print(" Режим: dry-run (запросы к SES не выполнялись)")
return 0
# Полноценный прогон
try:
return asyncio.run(_async_main(args, scenario))
except KeyboardInterrupt:
print("\nПрервано пользователем.", file=sys.stderr)
return 1
async def _async_main(args: argparse.Namespace, scenario) -> int:
"""Асинхронное ядро: создаёт клиент, прогоняет шаги, выводит результат."""
ses_config = SESConfig(
base_url=args.ses_url,
channel=scenario.channel or args.channel,
request_timeout=scenario.timeout,
)
client = SESClient(ses_config, verify=not args.no_verify)
session_id: str = args.session or f"monitoring_test_{int(time.time())}"
try:
# -------------------------------
# Шапка
print()
print(f"=== Сценарий: '{scenario.name}' ===")
print(f"SES: {args.ses_url}")
print(f"Robot ID: {scenario.robot_id}")
print(f"Канал: {scenario.channel or 'monitoring'}")
print(f"Шагов: {len(scenario.steps)}")
print(f"Сессия: {session_id}")
if args.no_verify:
print("SSL: проверка ОТКЛЮЧЕНА")
print()
# -------------------------------
# Пошаговое выполнение
all_passed = True
failed_step = 0
for i, step in enumerate(scenario.steps):
step_num = i + 1
total = len(scenario.steps)
print(f"── Шаг {step_num}/{total} {''*40}")
print(f"{step.send}")
with timer() as t:
response = await client.ask(
text=step.send,
session_id=session_id,
robot_id=scenario.robot_id,
channel=scenario.channel,
restart_closed=scenario.restart_closed,
)
response_time_ms = t.get("elapsed", 0) * 1000
print(f"← Время ответа: {response_time_ms:.0f}ms")
if args.verbose:
raw_json = json.dumps(response.raw, ensure_ascii=False, indent=2)
print("← RAW JSON:")
print(raw_json)
print()
if response.error != 0:
print(f" [FAIL] SES API error={response.error}")
all_passed = False
failed_step = step_num
break
if not step.expect:
print(" [PASS] OK (без проверок)\n")
continue
# Проверяем ожидания
results = _format_expectation_results(step.expect, response.raw)
step_passed = True
for line in results:
print(line)
if "[FAIL]" in line:
step_passed = False
print()
if not step_passed:
all_passed = False
failed_step = step_num
break
# -------------------------------
# Итог
print(f"{''*50}")
if all_passed:
print(
f"РЕЗУЛЬТАТ: УСПЕШНО "
f"({len(scenario.steps)}/{len(scenario.steps)} шагов)"
)
return 0
else:
print(f"РЕЗУЛЬТАТ: ПРОВАЛЕН (шаг {failed_step})")
return 1
finally:
await client.close()
if __name__ == "__main__":
sys.exit(main())
+33 -20
View File
@@ -103,15 +103,6 @@ class ScenarioStep:
# contains/equals/not_contains/not_empty/regex: условие # contains/equals/not_contains/not_empty/regex: условие
@dataclass
class ArchiveCheckConfig:
"""Настройки проверки архива для сценария."""
enabled: bool = False
lookback_minutes: int = 10
expect: List[Dict[str, Any]] = field(default_factory=list)
@dataclass @dataclass
class ScenarioConfig: class ScenarioConfig:
"""Конфигурация одного сценария мониторинга.""" """Конфигурация одного сценария мониторинга."""
@@ -125,7 +116,6 @@ class ScenarioConfig:
channel: Optional[str] = None channel: Optional[str] = None
restart_closed: bool = False restart_closed: bool = False
steps: List[ScenarioStep] = field(default_factory=list) steps: List[ScenarioStep] = field(default_factory=list)
archive_check: Optional[ArchiveCheckConfig] = None
def _dict_to_scenario_step(data: Dict[str, Any]) -> ScenarioStep: def _dict_to_scenario_step(data: Dict[str, Any]) -> ScenarioStep:
@@ -140,15 +130,6 @@ def _dict_to_scenario_config(data: Dict[str, Any]) -> ScenarioConfig:
"""Преобразует словарь из YAML в ScenarioConfig.""" """Преобразует словарь из YAML в ScenarioConfig."""
steps = [_dict_to_scenario_step(s) for s in data.get("steps", [])] steps = [_dict_to_scenario_step(s) for s in data.get("steps", [])]
archive_data = data.get("archive_check")
archive_check = None
if archive_data:
archive_check = ArchiveCheckConfig(
enabled=archive_data.get("enabled", False),
lookback_minutes=archive_data.get("lookback_minutes", 10),
expect=archive_data.get("expect", []),
)
return ScenarioConfig( return ScenarioConfig(
name=data.get("name", ""), name=data.get("name", ""),
description=data.get("description", ""), description=data.get("description", ""),
@@ -159,7 +140,6 @@ def _dict_to_scenario_config(data: Dict[str, Any]) -> ScenarioConfig:
channel=data.get("channel"), channel=data.get("channel"),
restart_closed=data.get("restart_closed", False), restart_closed=data.get("restart_closed", False),
steps=steps, steps=steps,
archive_check=archive_check,
) )
@@ -297,3 +277,36 @@ def load_scenarios(scenarios_dir: str) -> List[ScenarioConfig]:
raise RuntimeError(f"Ошибка чтения сценария {yml_file}: {e}") from e raise RuntimeError(f"Ошибка чтения сценария {yml_file}: {e}") from e
return scenarios return scenarios
def load_scenario_file(path: str) -> ScenarioConfig:
"""Загружает один YAML-файл сценария.
Предназначена для CLI-утилиты ses-test. В отличие от load_scenarios(),
не фильтрует по флагу enabled — сценарий загружается всегда.
Args:
path: Путь к YAML-файлу сценария.
Returns:
ScenarioConfig с заполненными полями.
Raises:
FileNotFoundError: Файл не найден.
yaml.YAMLError: Ошибка парсинга YAML.
RuntimeError: Ошибка структуры сценария.
"""
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"Файл сценария не найден: {path}")
with open(file_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if data is None:
raise RuntimeError(f"Файл сценария пуст: {path}")
try:
return _dict_to_scenario_config(data)
except (yaml.YAMLError, KeyError) as e:
raise RuntimeError(f"Ошибка чтения сценария {path}: {e}") from e
+30 -30
View File
@@ -2,10 +2,9 @@
Главный цикл watchdog-демона для мониторинга SES. Главный цикл watchdog-демона для мониторинга SES.
Архитектура: Архитектура:
- Бесконечный asyncio-цикл с тремя параллельными задачами: - Бесконечный asyncio-цикл с двумя параллельными задачами:
1. liveness_task: быстрые проверки каждые 30 секунд. 1. liveness_task: быстрые проверки каждые 30 секунд.
2. scenarios_task: запуск сценариев по их индивидуальному расписанию. 2. scenarios_task: запуск сценариев по их индивидуальному расписанию.
3. archive_task: анализ архива каждые 10 минут.
- Hot-reload сценариев из каталога каждые 60 секунд. - Hot-reload сценариев из каталога каждые 60 секунд.
- Все метрики отправляются в Zabbix единым батчем после проверок. - Все метрики отправляются в Zabbix единым батчем после проверок.
- Graceful shutdown по SIGTERM/SIGINT. - Graceful shutdown по SIGTERM/SIGINT.
@@ -93,6 +92,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 +240,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:
"""Выполняет проверку логов (если включено).""" """Выполняет проверку логов (если включено)."""
+3 -1
View File
@@ -135,11 +135,12 @@ class SESClient:
Все методы асинхронные для работы в asyncio-цикле. Все методы асинхронные для работы в asyncio-цикле.
""" """
def __init__(self, config: SESConfig): def __init__(self, config: SESConfig, verify: bool = True):
self._base_url = config.base_url.rstrip("/") self._base_url = config.base_url.rstrip("/")
self._channel = config.channel self._channel = config.channel
self._timeout = config.request_timeout self._timeout = config.request_timeout
self._robot_list_endpoint = config.robot_list_endpoint self._robot_list_endpoint = config.robot_list_endpoint
self._verify = verify
self._client: Optional[httpx.AsyncClient] = None self._client: Optional[httpx.AsyncClient] = None
@@ -149,6 +150,7 @@ class SESClient:
self._client = httpx.AsyncClient( self._client = httpx.AsyncClient(
timeout=httpx.Timeout(self._timeout), timeout=httpx.Timeout(self._timeout),
limits=httpx.Limits(max_keepalive_connections=5), limits=httpx.Limits(max_keepalive_connections=5),
verify=self._verify,
) )
return self._client return self._client
+38 -1
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
+1 -1
View File
@@ -13,7 +13,7 @@ Group=root
WorkingDirectory=/opt/ses-monitor WorkingDirectory=/opt/ses-monitor
# Запуск через venv # Запуск через venv
ExecStart=/opt/ses-monitor/venv/bin/python3 -m ses_monitor ExecStart=/opt/ses-monitor/.venv/bin/python3 -m ses_monitor
# Перезапуск при падении # Перезапуск при падении
Restart=always Restart=always
+5 -10
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
# =============================================================================
+20 -10
View File
@@ -112,6 +112,15 @@
<value_type>FLOAT</value_type> <value_type>FLOAT</value_type>
<units>d</units> <units>d</units>
<description>Количество дней до окончания лицензии SES. -1 = бессрочная.</description> <description>Количество дней до окончания лицензии SES. -1 = бессрочная.</description>
<triggers>
<trigger>
<uuid>82479a2d-72d3-4935-905e-5aa5ac2c24e3</uuid>
<expression>last(/SES Monitor/ses.license.days_remaining)&gt;0 and last(/SES Monitor/ses.license.days_remaining)&lt;30</expression>
<name>SES: лицензия истекает через (&lt;30 дней) на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -137,15 +146,7 @@
<trends>30d</trends> <trends>30d</trends>
<value_type>UNSIGNED</value_type> <value_type>UNSIGNED</value_type>
<description>1 — все сценарии прошли, 0 — есть упавшие.</description> <description>1 — все сценарии прошли, 0 — есть упавшие.</description>
<triggers>
<trigger>
<uuid>d2699acb49dd4af3af88fbcb96c6bf23</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>
<item> <item>
@@ -173,7 +174,7 @@
<triggers> <triggers>
<trigger> <trigger>
<uuid>b5a9deae3de74d68b3a92617541bf0c8</uuid> <uuid>b5a9deae3de74d68b3a92617541bf0c8</uuid>
<expression>last(/SES Monitor/ses.scenario.failed)&gt;0</expression> <expression>avg(/SES Monitor/ses.scenario.failed,#3)&gt;0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name> <name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>WARNING</priority> <priority>WARNING</priority>
<manual_close>YES</manual_close> <manual_close>YES</manual_close>
@@ -217,6 +218,15 @@
<trends>0</trends> <trends>0</trends>
<value_type>TEXT</value_type> <value_type>TEXT</value_type>
<description>Текст ошибки: "N сценариев упали". Пустая строка если всё хорошо.</description> <description>Текст ошибки: "N сценариев упали". Пустая строка если всё хорошо.</description>
<triggers>
<trigger>
<uuid>71bad15f-2b6e-4130-8567-4fd88f419bb5</uuid>
<expression>last(/SES Monitor/ses.scenario.error) &lt;&gt; ''</expression>
<name>SES: критическая ошибка выполнения сценариев на {HOST.NAME}</name>
<priority>DISASTER</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
</items> </items>
+18 -9
View File
@@ -108,6 +108,14 @@
<value_type>FLOAT</value_type> <value_type>FLOAT</value_type>
<units>d</units> <units>d</units>
<description>Количество дней до окончания лицензии SES. -1 = бессрочная.</description> <description>Количество дней до окончания лицензии SES. -1 = бессрочная.</description>
<triggers>
<trigger>
<expression>{SES Monitor:ses.license.days_remaining.last()}&gt;0 and {SES Monitor:ses.license.days_remaining.last()}&lt;30</expression>
<name>SES: лицензия истекает через (&lt;30 дней) на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -133,14 +141,7 @@
<trends>31536000</trends> <trends>31536000</trends>
<value_type>UNSIGNED</value_type> <value_type>UNSIGNED</value_type>
<description>1 — все сценарии прошли, 0 — есть упавшие.</description> <description>1 — все сценарии прошли, 0 — есть упавшие.</description>
<triggers>
<trigger>
<expression>{SES Monitor:ses.scenario.ok.last()}=0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>AVERAGE</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -165,7 +166,7 @@
<description>Количество упавших сценариев за последний цикл.</description> <description>Количество упавших сценариев за последний цикл.</description>
<triggers> <triggers>
<trigger> <trigger>
<expression>{SES Monitor:ses.scenario.failed.last()}&gt;0</expression> <expression>{SES Monitor:ses.scenario.failed.avg(#3)}&gt;0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name> <name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>WARNING</priority> <priority>WARNING</priority>
<manual_close>YES</manual_close> <manual_close>YES</manual_close>
@@ -206,6 +207,14 @@
<trends>31536000</trends> <trends>31536000</trends>
<value_type>TEXT</value_type> <value_type>TEXT</value_type>
<description>Текст ошибки: "N сценариев упали". Пустая строка если всё хорошо.</description> <description>Текст ошибки: "N сценариев упали". Пустая строка если всё хорошо.</description>
<triggers>
<trigger>
<expression>{SES Monitor:ses.scenario.error.last()} &lt;&gt; ''</expression>
<name>SES: критическая ошибка выполнения сценариев на {HOST.NAME}</name>
<priority>5</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
</items> </items>
+82 -12
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<zabbix_export> <zabbix_export>
<version>5.0</version> <version>6.0</version>
<date>2026-06-19T12:00:00Z</date> <date>2026-06-19T12:00:00Z</date>
<groups> <groups>
<group> <group>
@@ -60,13 +60,23 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.api.liveness"]</params> <parameters>
<parameter>$["ses.api.liveness"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
<trends>31536000</trends> <trends>31536000</trends>
<value_type>UNSIGNED</value_type> <value_type>UNSIGNED</value_type>
<key>ses.api.liveness</key> <key>ses.api.liveness</key>
<triggers>
<trigger>
<expression>{SES Monitor:ses.api.liveness.last()}=0</expression>
<name>SES API недоступен на {HOST.NAME}</name>
<priority>HIGH</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -78,7 +88,9 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.api.response_time"]</params> <parameters>
<parameter>$["ses.api.response_time"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
@@ -86,6 +98,14 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<value_type>FLOAT</value_type> <value_type>FLOAT</value_type>
<units>ms</units> <units>ms</units>
<key>ses.api.response_time</key> <key>ses.api.response_time</key>
<triggers>
<trigger>
<expression>{SES Monitor:ses.api.response_time.last()}&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>
<item> <item>
@@ -97,13 +117,23 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.license.ok"]</params> <parameters>
<parameter>$["ses.license.ok"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
<trends>31536000</trends> <trends>31536000</trends>
<value_type>UNSIGNED</value_type> <value_type>UNSIGNED</value_type>
<key>ses.license.ok</key> <key>ses.license.ok</key>
<triggers>
<trigger>
<expression>{SES Monitor:ses.license.ok.last()}=0</expression>
<name>SES: проблема с лицензией на {HOST.NAME}</name>
<priority>HIGH</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -115,7 +145,9 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.license.days_remaining"]</params> <parameters>
<parameter>$["ses.license.days_remaining"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
@@ -123,6 +155,14 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<value_type>FLOAT</value_type> <value_type>FLOAT</value_type>
<units>d</units> <units>d</units>
<key>ses.license.days_remaining</key> <key>ses.license.days_remaining</key>
<triggers>
<trigger>
<expression>{SES Monitor:ses.license.days_remaining.last()}&gt;0 and {SES Monitor:ses.license.days_remaining.last()}&lt;30</expression>
<name>SES: лицензия истекает через (&lt;30 дней) на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -134,7 +174,9 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.robots.count"]</params> <parameters>
<parameter>$["ses.robots.count"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
@@ -154,13 +196,16 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.scenario.ok"]</params> <parameters>
<parameter>$["ses.scenario.ok"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
<trends>31536000</trends> <trends>31536000</trends>
<value_type>UNSIGNED</value_type> <value_type>UNSIGNED</value_type>
<key>ses.scenario.ok</key> <key>ses.scenario.ok</key>
</item> </item>
<item> <item>
@@ -172,7 +217,9 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.scenario.total"]</params> <parameters>
<parameter>$["ses.scenario.total"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
@@ -190,13 +237,29 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.scenario.failed"]</params> <parameters>
<parameter>$["ses.scenario.failed"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
<trends>31536000</trends> <trends>31536000</trends>
<value_type>UNSIGNED</value_type> <value_type>UNSIGNED</value_type>
<key>ses.scenario.failed</key> <key>ses.scenario.failed</key>
<triggers>
<trigger>
<expression>{SES Monitor:ses.scenario.failed.avg(#3)}&gt;0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
<trigger>
<expression>{SES Monitor:ses.scenario.failed.avg(#3)}&gt;0</expression>
<name>SES: критическая ошибка выполнения сценариев на {HOST.NAME}</name>
<priority>DISASTER</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item> </item>
<item> <item>
@@ -208,7 +271,9 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.scenario.avg_time"]</params> <parameters>
<parameter>$["ses.scenario.avg_time"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
@@ -227,7 +292,9 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.scenario.max_time"]</params> <parameters>
<parameter>$["ses.scenario.max_time"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
@@ -246,13 +313,16 @@ JSON с метриками пишется в /var/tmp/ses_monitor/metrics.json
<preprocessing> <preprocessing>
<step> <step>
<type>JSONPATH</type> <type>JSONPATH</type>
<params>$["ses.scenario.error"]</params> <parameters>
<parameter>$["ses.scenario.error"]</parameter>
</parameters>
</step> </step>
</preprocessing> </preprocessing>
<history>7d</history> <history>7d</history>
<trends>31536000</trends> <trends>31536000</trends>
<value_type>TEXT</value_type> <value_type>TEXT</value_type>
<key>ses.scenario.error</key> <key>ses.scenario.error</key>
</item> </item>
</items> </items>
+361
View File
@@ -0,0 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?>
<zabbix_export>
<version>6.0</version>
<groups>
<group>
<uuid>88166f35c76b44e2b8ad333b3b4b9692</uuid>
<name>Templates</name>
</group>
</groups>
<templates>
<template>
<uuid>f4e7d78a816346dba7cfe9a79df9a740</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>b5d05b4d7b9d4e6398de7fe507748eaa</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>b6a649246ecf406a80e1cb5ad8f4d9e4</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>
<parameters>
<parameter>$["ses.api.liveness"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>0 — API недоступен, 1 — работает. Проверяется GET /ses/hostname.</description>
<triggers>
<trigger>
<uuid>b5798e51dc194d90aa6845107a7945e5</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>ce800e8914a84084bd160abd594b8ef4</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>
<parameters>
<parameter>$["ses.api.response_time"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>ms</units>
<description>Время ответа GET /ses/hostname в миллисекундах.</description>
<triggers>
<trigger>
<uuid>6813663c2e564b42aecd3dfd54d99ffb</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>05fa180f9e014fdeb73cfa7b1d9522f8</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>
<parameters>
<parameter>$["ses.license.ok"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>0 — проблема с лицензией, 1 — в порядке.</description>
<triggers>
<trigger>
<uuid>863d9b4b8a9d4e0c9b9dfd3c8022df82</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>5c090eb861524fb5a49aebf7d803f975</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>
<parameters>
<parameter>$["ses.license.days_remaining"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>d</units>
<description>Количество дней до окончания лицензии SES. -1 = бессрочная.</description>
<triggers>
<trigger>
<uuid>67f94034c31f480faa4768c986bdb2e9</uuid>
<expression>last(/SES Monitor/ses.license.days_remaining)&gt;0 and last(/SES Monitor/ses.license.days_remaining)&lt;30</expression>
<name>SES: лицензия истекает через (&lt;30 дней) на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>696ac7fe11f342f9a56ec2533bc7d71c</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>
<parameters>
<parameter>$["ses.robots.count"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>Общее количество зарегистрированных сценариев (роботов).
Изменение значения может сигнализировать о проблемах конфигурации.</description>
</item>
<!-- Сценарии -->
<item>
<uuid>48930571519a4071adecfb22a732eaad</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>
<parameters>
<parameter>$["ses.scenario.ok"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>1 — все сценарии прошли, 0 — есть упавшие.</description>
</item>
<item>
<uuid>b28ac0878070419595ea99ccb24fa370</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>
<parameters>
<parameter>$["ses.scenario.total"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>Количество активных сценариев мониторинга.</description>
</item>
<item>
<uuid>5b003d5ff88b47e3a3dc17d196036351</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>
<parameters>
<parameter>$["ses.scenario.failed"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>UNSIGNED</value_type>
<description>Количество упавших сценариев за последний цикл.</description>
<triggers>
<trigger>
<uuid>4de7a731b6b741c685a271a5f1a45a66</uuid>
<expression>avg(/SES Monitor/ses.scenario.failed,#3)&gt;0</expression>
<name>SES: тестовые сценарии мониторинга падают на {HOST.NAME}</name>
<priority>WARNING</priority>
<manual_close>YES</manual_close>
</trigger>
<trigger>
<uuid>89a0f35dcfc74d58a0bf96d9c56c85a2</uuid>
<expression>avg(/SES Monitor/ses.scenario.failed,#3)&gt;0</expression>
<name>SES: критическая ошибка выполнения сценариев на {HOST.NAME}</name>
<priority>DISASTER</priority>
<manual_close>YES</manual_close>
</trigger>
</triggers>
</item>
<item>
<uuid>0f911f5a7529495db35ad1784a32a0de</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>
<parameters>
<parameter>$["ses.scenario.avg_time"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>ms</units>
<description>Среднее время выполнения сценария (миллисекунды).</description>
</item>
<item>
<uuid>5747ec81622c44dc8ecac9be38458dad</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>
<parameters>
<parameter>$["ses.scenario.max_time"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>30d</trends>
<value_type>FLOAT</value_type>
<units>ms</units>
<description>Максимальное время выполнения сценария (миллисекунды).</description>
</item>
<item>
<uuid>bafb3afc2ac446089f5bd49d88960921</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>
<parameters>
<parameter>$["ses.scenario.error"]</parameter>
</parameters>
</step>
</preprocessing>
<history>7d</history>
<trends>0</trends>
<value_type>TEXT</value_type>
<description>Текст ошибки: "N сценариев упали". Пустая строка если всё хорошо.</description>
</item>
</items>
</template>
</templates>
</zabbix_export>