Implement AnonBot integration and monitoring enhancements

- Added AnonBot service to docker-compose with resource limits and environment variables.
- Updated Makefile to include commands for AnonBot logs, restart, and dependency checks.
- Enhanced Grafana dashboards with AnonBot health metrics and database connection statistics.
- Implemented AnonBot status retrieval in the message sender for improved monitoring.
- Updated Prometheus configuration to scrape metrics from AnonBot service.
This commit was merged in pull request #2.
This commit is contained in:
2025-09-08 23:17:24 +03:00
parent 40968dd075
commit b34da5015d
9 changed files with 2187 additions and 206 deletions

View File

@@ -1,188 +0,0 @@
# PID Manager - Управление процессами ботов
## Описание
`pid_manager.py` - это общий модуль для управления PID файлами всех ботов в проекте. Он обеспечивает создание, отслеживание и очистку PID файлов для мониторинга состояния процессов.
## Использование
### Для новых ботов
Чтобы добавить PID мониторинг в новый бот, выполните следующие шаги:
1. **Импортируйте PID менеджер в ваш скрипт запуска:**
```python
import sys
import os
# Добавляем путь к инфраструктуре в sys.path
infra_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'infra', 'monitoring')
if infra_path not in sys.path:
sys.path.insert(0, infra_path)
from pid_manager import get_bot_pid_manager
```
2. **Создайте PID менеджер в начале main функции:**
```python
async def main():
# Создаем PID менеджер для отслеживания процесса (если доступен)
pid_manager = None
if get_bot_pid_manager:
pid_manager = get_bot_pid_manager("your_bot_name") # Замените на имя вашего бота
if not pid_manager.create_pid_file():
logger.error("Не удалось создать PID файл, завершаем работу")
return
else:
logger.info("PID менеджер недоступен, запуск без PID файла")
# Ваш код запуска бота...
```
3. **Очистите PID файл при завершении:**
```python
try:
# Ваш код работы бота...
finally:
# Очищаем PID файл (если PID менеджер доступен)
if pid_manager:
pid_manager.cleanup_pid_file()
```
### Для мониторинга
Чтобы добавить новый бот в систему мониторинга:
```python
from infra.monitoring.metrics_collector import MetricsCollector
# Создаем экземпляр коллектора метрик
collector = MetricsCollector()
# Добавляем новый бот в мониторинг
collector.add_bot_to_monitoring("your_bot_name")
# Теперь можно проверять статус
status, uptime = collector.check_process_status("your_bot_name")
```
## Структура файлов
```
prod/
├── infra/
│ └── monitoring/
│ ├── pid_manager.py # Основной модуль
│ ├── metrics_collector.py # Мониторинг процессов
│ └── README_PID_MANAGER.md # Эта документация
├── bots/
│ ├── telegram-helper-bot/
│ │ └── run_helper.py # Использует PID менеджер
│ └── your-new-bot/
│ └── run_your_bot.py # Будет использовать PID менеджер
├── helper_bot.pid # PID файл helper_bot
├── your_bot.pid # PID файл вашего бота
└── .gitignore # Содержит *.pid
```
## API
### PIDManager
- `create_pid_file()` - Создает PID файл
- `cleanup_pid_file()` - Удаляет PID файл
- `is_running()` - Проверяет, запущен ли процесс
- `get_pid()` - Получает PID из файла
### Функции
- `get_bot_pid_manager(bot_name)` - Создает PID менеджер для бота
- `create_pid_manager(process_name, project_root)` - Создает PID менеджер с настройками
## Примеры
### Простой бот
```python
import asyncio
from pid_manager import get_bot_pid_manager
async def main():
# Создаем PID менеджер
pid_manager = get_bot_pid_manager("simple_bot")
if not pid_manager.create_pid_file():
print("Не удалось создать PID файл")
return
try:
# Ваш код бота
print("Бот запущен...")
await asyncio.sleep(3600) # Работаем час
finally:
# Очищаем PID файл
pid_manager.cleanup_pid_file()
if __name__ == '__main__':
asyncio.run(main())
```
### Бот с обработкой сигналов
```python
import asyncio
import signal
from pid_manager import get_bot_pid_manager
async def main():
pid_manager = get_bot_pid_manager("advanced_bot")
if not pid_manager.create_pid_file():
return
# Флаг для корректного завершения
shutdown_event = asyncio.Event()
def signal_handler(signum, frame):
print(f"Получен сигнал {signum}, завершаем работу...")
shutdown_event.set()
# Регистрируем обработчики сигналов
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
# Ваш код бота
await shutdown_event.wait()
finally:
pid_manager.cleanup_pid_file()
if __name__ == '__main__':
asyncio.run(main())
```
## Примечания
- PID файлы создаются в корне проекта
- Все PID файлы автоматически игнорируются Git (см. `.gitignore`)
- PID менеджер автоматически обрабатывает сигналы SIGTERM и SIGINT
- При завершении процесса PID файл автоматически удаляется
- Система мониторинга автоматически находит PID файлы в корне проекта
## Изолированный запуск
При запуске бота изолированно (без доступа к основному проекту):
- PID менеджер автоматически не создается
- Бот запускается без PID файла
- В логах появляется сообщение "PID менеджер недоступен (изолированный запуск), PID файл не создается"
- Это позволяет запускать бота в любой среде без ошибок
## Автоматическое определение
Система автоматически определяет доступность PID менеджера:
1. **В основном проекте**: PID менеджер доступен, создается PID файл для мониторинга
2. **Изолированно**: PID менеджер недоступен, бот работает без PID файла
3. **Fallback**: Если PID менеджер недоступен, бот продолжает работать нормально

View File

@@ -64,6 +64,39 @@ class MessageSender:
logger.error(f"Ошибка при отправке сообщения в Telegram: {e}")
return False
async def get_anonbot_status(self) -> Tuple[str, str]:
"""Получение статуса AnonBot через HTTP API"""
try:
async with aiohttp.ClientSession() as session:
# AnonBot доступен через Docker network
url = "http://bots_anon_bot:8081/status"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200:
data = await response.json()
status = data.get('status', 'unknown')
uptime = data.get('uptime', 'unknown')
# Форматируем статус с эмодзи
if status == 'running':
status_emoji = ""
elif status == 'stopped':
status_emoji = ""
else:
status_emoji = "⚠️"
return f"{status_emoji}", uptime
else:
logger.warning(f"AnonBot API вернул статус {response.status}")
return "⚠️ AnonBot", "API недоступен"
except aiohttp.ClientError as e:
logger.warning(f"Ошибка подключения к AnonBot API: {e}")
return "", "Недоступен"
except Exception as e:
logger.error(f"Неожиданная ошибка при получении статуса AnonBot: {e}")
return "⚠️", "Ошибка"
def should_send_status(self) -> bool:
"""Проверка, нужно ли отправить статус (каждые N минут)"""
now = datetime.now()
@@ -147,11 +180,14 @@ class MessageSender:
else:
return "🚨"
def get_status_message(self, system_info: Dict) -> str:
async def get_status_message(self, system_info: Dict) -> str:
"""Формирование сообщения со статусом сервера"""
try:
helper_bot_status, helper_bot_uptime = self.metrics_collector.check_process_status('helper_bot')
# Получаем статус AnonBot
anonbot_status, anonbot_uptime = await self.get_anonbot_status()
# Получаем эмодзи для всех метрик
cpu_emoji = self._get_cpu_emoji(system_info['cpu_percent'])
ram_emoji = self._get_memory_emoji(system_info['ram_percent'])
@@ -183,6 +219,7 @@ Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_wri
**🤖 Процессы:**
{helper_bot_status} helper-bot - {helper_bot_uptime}
{anonbot_status} AnonBot - {anonbot_uptime}
---------------------------------
⏰ Uptime сервера: {system_info['system_uptime']}
🔍 Уровень мониторинга: {level_text} ({monitoring_level})"""
@@ -259,7 +296,7 @@ Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_wri
logger.error("Не удалось получить информацию о системе")
return False
status_message = self.get_status_message(system_info)
status_message = await self.get_status_message(system_info)
success = await self.send_telegram_message(self.group_for_logs, status_message)
# Обновляем время последней отправки только при успешной отправке

View File

@@ -590,14 +590,17 @@ class MetricsCollector:
alerts.append(('cpu', system_info['cpu_percent'], f"Нагрузка за 1 мин: {system_info['load_avg_1m']}"))
logger.warning(f"CPU ALERT: {system_info['cpu_percent']:.1f}% > {self.threshold}% (задержка {self.alert_delays['cpu']}s)")
else:
# CPU ниже порога - сбрасываем состояние
# CPU ниже порога - сбрасываем состояние только если был активный алерт
if self.alert_states['cpu']:
self.alert_states['cpu'] = False
recoveries.append(('cpu', system_info['cpu_percent']))
logger.info(f"CPU RECOVERY: {system_info['cpu_percent']:.1f}% < {self.recovery_threshold}%")
# Сбрасываем время начала превышения
self.alert_start_times['cpu'] = None
# Сбрасываем время начала превышения только после отправки алерта
self.alert_start_times['cpu'] = None
elif system_info['cpu_percent'] < self.recovery_threshold and self.alert_start_times['cpu'] is not None:
# Если CPU опустился значительно ниже порога, сбрасываем время начала превышения
logger.debug(f"CPU значительно ниже порога {self.recovery_threshold}%: {system_info['cpu_percent']:.1f}% - сбрасываем время начала превышения")
self.alert_start_times['cpu'] = None
# Проверка RAM с задержкой
if system_info['ram_percent'] > self.threshold:
@@ -613,14 +616,17 @@ class MetricsCollector:
alerts.append(('ram', system_info['ram_percent'], f"Используется: {system_info['ram_used']} GB из {system_info['ram_total']} GB"))
logger.warning(f"RAM ALERT: {system_info['ram_percent']:.1f}% > {self.threshold}% (задержка {self.alert_delays['ram']}s)")
else:
# RAM ниже порога - сбрасываем состояние
# RAM ниже порога - сбрасываем состояние только если был активный алерт
if self.alert_states['ram']:
self.alert_states['ram'] = False
recoveries.append(('ram', system_info['ram_percent']))
logger.info(f"RAM RECOVERY: {system_info['ram_percent']:.1f}% < {self.recovery_threshold}%")
# Сбрасываем время начала превышения
self.alert_start_times['ram'] = None
# Сбрасываем время начала превышения только после отправки алерта
self.alert_start_times['ram'] = None
elif system_info['ram_percent'] < self.recovery_threshold and self.alert_start_times['ram'] is not None:
# Если RAM опустился значительно ниже порога, сбрасываем время начала превышения
logger.debug(f"RAM значительно ниже порога {self.recovery_threshold}%: {system_info['ram_percent']:.1f}% - сбрасываем время начала превышения")
self.alert_start_times['ram'] = None
# Проверка диска с задержкой
if system_info['disk_percent'] > self.threshold:
@@ -636,14 +642,17 @@ class MetricsCollector:
alerts.append(('disk', system_info['disk_percent'], f"Свободно: {system_info['disk_free']} GB на /"))
logger.warning(f"DISK ALERT: {system_info['disk_percent']:.1f}% > {self.threshold}% (задержка {self.alert_delays['disk']}s)")
else:
# Диск ниже порога - сбрасываем состояние
# Диск ниже порога - сбрасываем состояние только если был активный алерт
if self.alert_states['disk']:
self.alert_states['disk'] = False
recoveries.append(('disk', system_info['disk_percent']))
logger.info(f"DISK RECOVERY: {system_info['disk_percent']:.1f}% < {self.recovery_threshold}%")
# Сбрасываем время начала превышения
self.alert_start_times['disk'] = None
# Сбрасываем время начала превышения только после отправки алерта
self.alert_start_times['disk'] = None
elif system_info['disk_percent'] < self.recovery_threshold and self.alert_start_times['disk'] is not None:
# Если диск опустился значительно ниже порога, сбрасываем время начала превышения
logger.debug(f"Disk значительно ниже порога {self.recovery_threshold}%: {system_info['disk_percent']:.1f}% - сбрасываем время начала превышения")
self.alert_start_times['disk'] = None
return alerts, recoveries