Move ses_monitor package into src/ directory for proper Python packaging standards (src-layout). - src/ses_monitor/ — package source - pyproject.toml: [tool.setuptools.packages.find] where = ["src"] - pyproject.toml: [tool.pytest.ini_options] pythonpath = ["src"] - 59 tests pass unchanged
184 lines
6.9 KiB
Python
184 lines
6.9 KiB
Python
"""
|
||
Быстрые liveness-проверки: доступность API, лицензия, количество роботов.
|
||
|
||
Запускаются с высокой частотой (каждые 30 секунд) и дают первый сигнал
|
||
о проблемах с SES сервером.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from ses_monitor.config import SESConfig
|
||
from ses_monitor.session_client import SESClient, HostnameResponse, LicenseResponse
|
||
|
||
log = logging.getLogger("ses_monitor.checker")
|
||
|
||
|
||
@dataclass
|
||
class LivenessResult:
|
||
"""Результат одного цикла liveness-проверок."""
|
||
|
||
# Доступность API
|
||
api_ok: bool = True
|
||
api_response_time_ms: float = 0.0
|
||
api_error: str = ""
|
||
|
||
# Лицензия
|
||
license_ok: bool = True
|
||
license_error: str = ""
|
||
license_days_remaining: int = -1
|
||
|
||
# Количество сценариев (роботов)
|
||
robots_count: int = 0
|
||
robots_error: str = ""
|
||
|
||
# Общая метка времени
|
||
timestamp: float = 0.0
|
||
|
||
@property
|
||
def all_ok(self) -> bool:
|
||
"""Все проверки прошли успешно."""
|
||
return self.api_ok and self.license_ok and not self.robots_error
|
||
|
||
|
||
class LivenessChecker:
|
||
"""Выполняет быстрые проверки работоспособности SES.
|
||
|
||
Проверяет:
|
||
1. GET /ses/hostname — жив ли сервер.
|
||
2. GET /license/check — не истекла ли лицензия.
|
||
3. GET /ses/robot/list — не пропали ли сценарии.
|
||
"""
|
||
|
||
def __init__(self, ses_config: SESConfig, robot_names: Optional[List[str]] = None):
|
||
self._ses_config = ses_config
|
||
# Эталонный список роботов (для сравнения)
|
||
self._expected_robots = robot_names
|
||
|
||
async def check(self, client: SESClient) -> LivenessResult:
|
||
"""Выполняет полный цикл liveness-проверок.
|
||
|
||
Args:
|
||
client: HTTP-клиент для SES API.
|
||
|
||
Returns:
|
||
LivenessResult со статусом всех проверок.
|
||
"""
|
||
import time
|
||
|
||
result = LivenessResult(timestamp=time.time())
|
||
|
||
# 1. Проверка hostname (доступность API)
|
||
await self._check_hostname(client, result)
|
||
|
||
# 2. Проверка лицензии
|
||
await self._check_license(client, result)
|
||
|
||
# 3. Проверка количества роботов
|
||
await self._check_robots(client, result)
|
||
|
||
return result
|
||
|
||
async def _check_hostname(
|
||
self, client: SESClient, result: LivenessResult
|
||
) -> None:
|
||
"""Проверяет GET /ses/hostname и замеряет время ответа."""
|
||
import time
|
||
|
||
try:
|
||
t0 = time.monotonic()
|
||
resp = await client.get_hostname()
|
||
elapsed = (time.monotonic() - t0) * 1000
|
||
|
||
result.api_response_time_ms = round(elapsed, 1)
|
||
|
||
if resp.error == 0:
|
||
result.api_ok = True
|
||
log.debug(
|
||
"Liveness hostname OK: %s v%s, время=%dms",
|
||
resp.hostname, resp.version, round(elapsed),
|
||
)
|
||
else:
|
||
result.api_ok = False
|
||
result.api_error = f"API ошибка error={resp.error}: {resp.message}"
|
||
log.warning("Liveness hostname ошибка: %s", result.api_error)
|
||
|
||
except Exception as e:
|
||
result.api_ok = False
|
||
result.api_error = f"Недоступен: {e}"
|
||
log.error("Liveness hostname исключение: %s", e)
|
||
|
||
async def _check_license(
|
||
self, client: SESClient, result: LivenessResult
|
||
) -> None:
|
||
"""Проверяет GET /license/check."""
|
||
try:
|
||
resp = await client.get_license()
|
||
|
||
if resp.error == 0:
|
||
# "infinity" или число > 0 — лицензия в порядке
|
||
remaining = resp.remaining_licenses
|
||
if remaining == "infinity":
|
||
result.license_ok = True
|
||
result.license_days_remaining = -1
|
||
elif remaining.isdigit():
|
||
days = int(remaining)
|
||
result.license_days_remaining = days
|
||
if days > 0:
|
||
result.license_ok = True
|
||
else:
|
||
result.license_ok = False
|
||
result.license_error = (
|
||
f"Лицензия истекла: remaining={remaining}"
|
||
)
|
||
else:
|
||
result.license_ok = False
|
||
result.license_error = (
|
||
f"Лицензия истекла или исчерпана: remaining={remaining}"
|
||
)
|
||
log.debug("Liveness license OK: remaining=%s, days=%d", remaining, result.license_days_remaining)
|
||
else:
|
||
result.license_ok = False
|
||
result.license_error = f"Ошибка лицензии error={resp.error}"
|
||
log.warning("Liveness license ошибка: %s", result.license_error)
|
||
|
||
except Exception as e:
|
||
result.license_ok = False
|
||
result.license_error = f"Недоступна: {e}"
|
||
log.error("Liveness license исключение: %s", e)
|
||
|
||
async def _check_robots(
|
||
self, client: SESClient, result: LivenessResult
|
||
) -> None:
|
||
"""Проверяет GET /ses/robot/list — список сценариев."""
|
||
try:
|
||
robots = await client.get_robots()
|
||
result.robots_count = len(robots)
|
||
|
||
# Если задан эталонный список — проверяем имена
|
||
if self._expected_robots is not None:
|
||
actual_names = {r.get("name", "") for r in robots}
|
||
expected_set = set(self._expected_robots)
|
||
missing = expected_set - actual_names
|
||
extra = actual_names - expected_set
|
||
|
||
if missing:
|
||
result.robots_error = f"Отсутствуют роботы: {missing}"
|
||
log.warning("Liveness robots: не хватает %s", missing)
|
||
if extra:
|
||
msg = f"Новые роботы: {extra}"
|
||
if result.robots_error:
|
||
result.robots_error += "; " + msg
|
||
else:
|
||
result.robots_error = msg
|
||
log.info("Liveness robots: новые %s", extra)
|
||
|
||
log.debug("Liveness robots count: %d", result.robots_count)
|
||
|
||
except Exception as e:
|
||
result.robots_error = f"Недоступен: {e}"
|
||
log.error("Liveness robots исключение: %s", e)
|