"""Heartbeat для Docker healthcheck и ежечасный пинг «бот живой». Бот периодически обновляет mtime файла; healthcheck считает контейнер живым, если файл свежий. Если event loop завис — heartbeat перестаёт обновляться. Раз в час в чат логов (``ALERT_CHAT_ID``) уходит короткое сообщение о работе бота, и в аналитику пишется событие ``bot_alive``. """ from __future__ import annotations import asyncio import logging from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING from bot.services.alerts import notify_alert from bot.services.runtime import uptime_human, uptime_seconds from common import __version__ from common.analytics import EventType, record from common.config import get_settings from common.texts import alerts as alerts_text from common.texts.render import render if TYPE_CHECKING: from aiogram import Bot logger = logging.getLogger(__name__) #: Как часто обновлять heartbeat-файл. HEARTBEAT_INTERVAL = 30 #: Час (в таймзоне бота), за который пинг уже отправлен. _last_ping_hour: datetime | None = None async def ping_hourly(bot: Bot, now: datetime | None = None) -> bool: """Записать событие ``bot_alive`` и отправить пинг в чат логов раз в час. Возвращает ``True``, если в этом часу пинг отправлен (или предпринята попытка). Повторные вызовы в течение того же часа ничего не делают. """ global _last_ping_hour settings = get_settings() current_hour = (now or datetime.now(settings.tzinfo)).replace(minute=0, second=0, microsecond=0) if current_hour == _last_ping_hour: return False _last_ping_hour = current_hour record( EventType.BOT_ALIVE, payload={"app_env": settings.app_env, "uptime_seconds": uptime_seconds()}, ) text = render( alerts_text.MSG_ALIVE, {"version": __version__, "uptime": uptime_human()}, ) return await notify_alert(bot, text) async def run_heartbeat(bot: Bot) -> None: """Обновлять heartbeat-файл каждые ``HEARTBEAT_INTERVAL`` секунд и пинговать раз в час.""" path = Path(get_settings().heartbeat_path) while True: try: path.touch() except OSError: logger.exception("Не удалось обновить heartbeat-файл %s", path) await ping_hourly(bot) await asyncio.sleep(HEARTBEAT_INTERVAL)