""" Быстрые 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)