73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""Heartbeat для Docker healthcheck и ежечасный пинг «бот живой».
|
||
|
||
Бот периодически обновляет mtime файла; healthcheck считает контейнер живым,
|
||
если файл свежий. Если event loop завис — heartbeat перестаёт обновляться.
|
||
|
||
Раз в час в чат логов (``ALERT_CHAT_ID``) уходит короткое сообщение о работе бота,
|
||
и в ClickHouse пишется событие ``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)
|