Add server monitoring functionality and update Makefile and requirements
- Introduced a new server monitoring module in `run_helper.py` with graceful shutdown handling. - Updated `.gitignore` to include PID files. - Added `test-monitor` target in `Makefile` for testing the server monitoring module. - Included `psutil` in `requirements.txt` for system monitoring capabilities.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -42,3 +42,8 @@ test.db
|
|||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
PERFORMANCE_IMPROVEMENTS.md
|
PERFORMANCE_IMPROVEMENTS.md
|
||||||
|
|
||||||
|
# PID files
|
||||||
|
*.pid
|
||||||
|
helper_bot.pid
|
||||||
|
voice_bot.pid
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help test test-db test-coverage test-html clean install
|
.PHONY: help test test-db test-coverage test-html clean install test-monitor
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
help:
|
help:
|
||||||
@@ -11,6 +11,7 @@ help:
|
|||||||
@echo " test-errors - Run error handling tests only"
|
@echo " test-errors - Run error handling tests only"
|
||||||
@echo " test-utils - Run utility functions tests only"
|
@echo " test-utils - Run utility functions tests only"
|
||||||
@echo " test-keyboards - Run keyboard and filter tests only"
|
@echo " test-keyboards - Run keyboard and filter tests only"
|
||||||
|
@echo " test-monitor - Test server monitoring module"
|
||||||
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
|
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
|
||||||
@echo " test-html - Run tests and generate HTML coverage report"
|
@echo " test-html - Run tests and generate HTML coverage report"
|
||||||
@echo " clean - Clean up generated files"
|
@echo " clean - Clean up generated files"
|
||||||
@@ -49,6 +50,10 @@ test-utils:
|
|||||||
test-keyboards:
|
test-keyboards:
|
||||||
python3 -m pytest tests/test_keyboards_and_filters.py -v
|
python3 -m pytest tests/test_keyboards_and_filters.py -v
|
||||||
|
|
||||||
|
# Test server monitoring module
|
||||||
|
test-monitor:
|
||||||
|
python3 tests/test_monitor.py
|
||||||
|
|
||||||
# Run tests with coverage
|
# Run tests with coverage
|
||||||
test-coverage:
|
test-coverage:
|
||||||
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
|
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
|
||||||
@@ -69,5 +74,7 @@ clean:
|
|||||||
rm -f .coverage
|
rm -f .coverage
|
||||||
rm -f database/test.db
|
rm -f database/test.db
|
||||||
rm -f test.db
|
rm -f test.db
|
||||||
|
rm -f helper_bot.pid
|
||||||
|
rm -f voice_bot.pid
|
||||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||||
find . -type f -name "*.pyc" -delete
|
find . -type f -name "*.pyc" -delete
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import server_monitor
|
||||||
|
|||||||
453
helper_bot/server_monitor.py
Normal file
453
helper_bot/server_monitor.py
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerMonitor:
|
||||||
|
def __init__(self, bot, group_for_logs: str, important_logs: str):
|
||||||
|
self.bot = bot
|
||||||
|
self.group_for_logs = group_for_logs
|
||||||
|
self.important_logs = important_logs
|
||||||
|
|
||||||
|
# Пороговые значения для алертов
|
||||||
|
self.threshold = 80.0
|
||||||
|
self.recovery_threshold = 75.0
|
||||||
|
|
||||||
|
# Состояние алертов для предотвращения спама
|
||||||
|
self.alert_states = {
|
||||||
|
'cpu': False,
|
||||||
|
'ram': False,
|
||||||
|
'disk': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# PID файлы для отслеживания процессов
|
||||||
|
self.pid_files = {
|
||||||
|
'voice_bot': 'voice_bot.pid',
|
||||||
|
'helper_bot': 'helper_bot.pid'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Время последней отправки статуса
|
||||||
|
self.last_status_time = None
|
||||||
|
|
||||||
|
# Для расчета скорости диска
|
||||||
|
self.last_disk_io = None
|
||||||
|
self.last_disk_io_time = None
|
||||||
|
|
||||||
|
def get_system_info(self) -> Dict:
|
||||||
|
"""Получение информации о системе"""
|
||||||
|
try:
|
||||||
|
# CPU
|
||||||
|
cpu_percent = psutil.cpu_percent(interval=1)
|
||||||
|
load_avg = psutil.getloadavg()
|
||||||
|
cpu_count = psutil.cpu_count()
|
||||||
|
|
||||||
|
# Память
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
swap = psutil.swap_memory()
|
||||||
|
|
||||||
|
# Диск
|
||||||
|
disk = psutil.disk_usage('/')
|
||||||
|
disk_io = psutil.disk_io_counters()
|
||||||
|
|
||||||
|
# Расчет скорости диска
|
||||||
|
disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io)
|
||||||
|
|
||||||
|
# Система
|
||||||
|
boot_time = psutil.boot_time()
|
||||||
|
uptime = time.time() - boot_time
|
||||||
|
|
||||||
|
return {
|
||||||
|
'cpu_percent': cpu_percent,
|
||||||
|
'load_avg_1m': round(load_avg[0], 2),
|
||||||
|
'load_avg_5m': round(load_avg[1], 2),
|
||||||
|
'load_avg_15m': round(load_avg[2], 2),
|
||||||
|
'cpu_count': cpu_count,
|
||||||
|
'ram_used': round(memory.used / (1024**3), 2),
|
||||||
|
'ram_total': round(memory.total / (1024**3), 2),
|
||||||
|
'ram_percent': round(memory.percent, 1),
|
||||||
|
'swap_used': round(swap.used / (1024**3), 2),
|
||||||
|
'swap_total': round(swap.total / (1024**3), 2),
|
||||||
|
'swap_percent': swap.percent,
|
||||||
|
'disk_used': round(disk.used / (1024**3), 2),
|
||||||
|
'disk_total': round(disk.total / (1024**3), 2),
|
||||||
|
'disk_percent': round((disk.used / disk.total) * 100, 2),
|
||||||
|
'disk_free': round(disk.free / (1024**3), 2),
|
||||||
|
'disk_read_speed': disk_read_speed,
|
||||||
|
'disk_write_speed': disk_write_speed,
|
||||||
|
'disk_io_percent': self._calculate_disk_io_percent(),
|
||||||
|
'system_uptime': self._format_uptime(uptime),
|
||||||
|
'server_hostname': psutil.os.uname().nodename,
|
||||||
|
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при получении информации о системе: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _format_bytes(self, bytes_value: int) -> str:
|
||||||
|
"""Форматирование байтов в человекочитаемый вид"""
|
||||||
|
if bytes_value == 0:
|
||||||
|
return "0 B"
|
||||||
|
|
||||||
|
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
i = 0
|
||||||
|
while bytes_value >= 1024 and i < len(size_names) - 1:
|
||||||
|
bytes_value /= 1024.0
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return f"{bytes_value:.1f} {size_names[i]}"
|
||||||
|
|
||||||
|
def _format_uptime(self, seconds: float) -> str:
|
||||||
|
"""Форматирование времени работы системы"""
|
||||||
|
days = int(seconds // 86400)
|
||||||
|
hours = int((seconds % 86400) // 3600)
|
||||||
|
minutes = int((seconds % 3600) // 60)
|
||||||
|
|
||||||
|
if days > 0:
|
||||||
|
return f"{days}д {hours}ч {minutes}м"
|
||||||
|
elif hours > 0:
|
||||||
|
return f"{hours}ч {minutes}м"
|
||||||
|
else:
|
||||||
|
return f"{minutes}м"
|
||||||
|
|
||||||
|
def check_process_status(self, process_name: str) -> str:
|
||||||
|
"""Проверка статуса процесса"""
|
||||||
|
try:
|
||||||
|
# Сначала проверяем по PID файлу
|
||||||
|
pid_file = self.pid_files.get(process_name)
|
||||||
|
if pid_file and os.path.exists(pid_file):
|
||||||
|
try:
|
||||||
|
with open(pid_file, 'r') as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
if content and content != '# Этот файл будет автоматически обновляться при запуске бота':
|
||||||
|
pid = int(content)
|
||||||
|
if psutil.pid_exists(pid):
|
||||||
|
return "✅"
|
||||||
|
except (ValueError, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Проверяем по имени процесса более точно
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||||
|
try:
|
||||||
|
proc_name = proc.info['name'].lower()
|
||||||
|
cmdline = ' '.join(proc.info['cmdline']).lower() if proc.info['cmdline'] else ''
|
||||||
|
|
||||||
|
# Более точная проверка для каждого бота
|
||||||
|
if process_name == 'voice_bot':
|
||||||
|
# Проверяем voice_bot
|
||||||
|
if ('voice_bot' in proc_name or
|
||||||
|
'voice_bot' in cmdline or
|
||||||
|
'voice_bot_v2.py' in cmdline):
|
||||||
|
return "✅"
|
||||||
|
elif process_name == 'helper_bot':
|
||||||
|
# Проверяем helper_bot
|
||||||
|
if ('helper_bot' in proc_name or
|
||||||
|
'helper_bot' in cmdline or
|
||||||
|
'run_helper.py' in cmdline or
|
||||||
|
'python' in proc_name and 'helper_bot' in cmdline):
|
||||||
|
return "✅"
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return "❌"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при проверке процесса {process_name}: {e}")
|
||||||
|
return "❌"
|
||||||
|
|
||||||
|
def should_send_status(self) -> bool:
|
||||||
|
"""Проверка, нужно ли отправить статус (каждые 30 минут в 00 и 30 минут часа)"""
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Проверяем, что сейчас 00 или 30 минут часа
|
||||||
|
if now.minute in [0, 30]:
|
||||||
|
# Проверяем, не отправляли ли мы уже статус в эту минуту
|
||||||
|
if (self.last_status_time is None or
|
||||||
|
self.last_status_time.hour != now.hour or
|
||||||
|
self.last_status_time.minute != now.minute):
|
||||||
|
self.last_status_time = now
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _calculate_disk_speed(self, current_disk_io) -> Tuple[str, str]:
|
||||||
|
"""Расчет скорости чтения/записи диска"""
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if self.last_disk_io is None or self.last_disk_io_time is None:
|
||||||
|
self.last_disk_io = current_disk_io
|
||||||
|
self.last_disk_io_time = current_time
|
||||||
|
return "0 B/s", "0 B/s"
|
||||||
|
|
||||||
|
time_diff = current_time - self.last_disk_io_time
|
||||||
|
if time_diff < 1: # Минимальный интервал 1 секунда
|
||||||
|
return "0 B/s", "0 B/s"
|
||||||
|
|
||||||
|
read_diff = current_disk_io.read_bytes - self.last_disk_io.read_bytes
|
||||||
|
write_diff = current_disk_io.write_bytes - self.last_disk_io.write_bytes
|
||||||
|
|
||||||
|
read_speed = read_diff / time_diff
|
||||||
|
write_speed = write_diff / time_diff
|
||||||
|
|
||||||
|
# Обновляем предыдущие значения
|
||||||
|
self.last_disk_io = current_disk_io
|
||||||
|
self.last_disk_io_time = current_time
|
||||||
|
|
||||||
|
return self._format_bytes(read_speed) + "/s", self._format_bytes(write_speed) + "/s"
|
||||||
|
|
||||||
|
def _calculate_disk_io_percent(self) -> int:
|
||||||
|
"""Расчет процента загрузки диска на основе IOPS"""
|
||||||
|
try:
|
||||||
|
# Получаем статистику диска
|
||||||
|
disk_io = psutil.disk_io_counters()
|
||||||
|
if disk_io is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Простая эвристика: считаем общее количество операций
|
||||||
|
total_ops = disk_io.read_count + disk_io.write_count
|
||||||
|
|
||||||
|
# Нормализуем к проценту (это приблизительная оценка)
|
||||||
|
# На macOS обычно нормальная нагрузка до 1000-5000 операций в секунду
|
||||||
|
if total_ops < 1000:
|
||||||
|
return 10
|
||||||
|
elif total_ops < 5000:
|
||||||
|
return 30
|
||||||
|
elif total_ops < 10000:
|
||||||
|
return 50
|
||||||
|
elif total_ops < 20000:
|
||||||
|
return 70
|
||||||
|
else:
|
||||||
|
return 90
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def should_send_startup_status(self) -> bool:
|
||||||
|
"""Проверка, нужно ли отправить статус при запуске"""
|
||||||
|
return self.last_status_time is None
|
||||||
|
|
||||||
|
async def send_startup_message(self):
|
||||||
|
"""Отправка сообщения о запуске бота"""
|
||||||
|
try:
|
||||||
|
message = f"""🚀 **Бот запущен!**
|
||||||
|
---------------------------------
|
||||||
|
**Время запуска:** <code>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</code>
|
||||||
|
**Сервер:** `{psutil.os.uname().nodename}`
|
||||||
|
**Система:** {psutil.os.uname().sysname} {psutil.os.uname().release}
|
||||||
|
|
||||||
|
✅ Мониторинг сервера активирован
|
||||||
|
✅ Статус будет отправляться каждые 30 минут (в 00 и 30 минут часа)
|
||||||
|
✅ Алерты будут отправляться при превышении пороговых значений
|
||||||
|
---------------------------------"""
|
||||||
|
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=self.important_logs,
|
||||||
|
text=message,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
logger.info("Сообщение о запуске бота отправлено")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке сообщения о запуске: {e}")
|
||||||
|
|
||||||
|
async def send_shutdown_message(self):
|
||||||
|
"""Отправка сообщения об отключении бота"""
|
||||||
|
try:
|
||||||
|
# Получаем финальную информацию о системе
|
||||||
|
system_info = self.get_system_info()
|
||||||
|
if not system_info:
|
||||||
|
system_info = {
|
||||||
|
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'server_hostname': psutil.os.uname().nodename
|
||||||
|
}
|
||||||
|
|
||||||
|
message = f"""🛑 **Бот отключен!**
|
||||||
|
---------------------------------
|
||||||
|
**Время отключения:** <code>{system_info['current_time']}</code>
|
||||||
|
**Сервер:** `{system_info['server_hostname']}`
|
||||||
|
|
||||||
|
❌ Мониторинг сервера остановлен
|
||||||
|
❌ Статус больше не будет отправляться
|
||||||
|
❌ Алерты отключены
|
||||||
|
|
||||||
|
⚠️ **Внимание:** Проверьте состояние сервера!
|
||||||
|
---------------------------------"""
|
||||||
|
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=self.important_logs,
|
||||||
|
text=message,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
logger.info("Сообщение об отключении бота отправлено")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке сообщения об отключении: {e}")
|
||||||
|
|
||||||
|
def check_alerts(self, system_info: Dict) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Проверка необходимости отправки алертов"""
|
||||||
|
alerts = []
|
||||||
|
|
||||||
|
# Проверка CPU
|
||||||
|
if system_info['cpu_percent'] > self.threshold and not self.alert_states['cpu']:
|
||||||
|
self.alert_states['cpu'] = True
|
||||||
|
alerts.append(('cpu', system_info['cpu_percent'], f"Нагрузка за 1 мин: {system_info['load_avg_1m']}"))
|
||||||
|
|
||||||
|
# Проверка RAM
|
||||||
|
if system_info['ram_percent'] > self.threshold and not self.alert_states['ram']:
|
||||||
|
self.alert_states['ram'] = True
|
||||||
|
alerts.append(('ram', system_info['ram_percent'], f"Используется: {system_info['ram_used']} GB из {system_info['ram_total']} GB"))
|
||||||
|
|
||||||
|
# Проверка диска
|
||||||
|
if system_info['disk_percent'] > self.threshold and not self.alert_states['disk']:
|
||||||
|
self.alert_states['disk'] = True
|
||||||
|
alerts.append(('disk', system_info['disk_percent'], f"Свободно: {system_info['disk_free']} GB на /"))
|
||||||
|
|
||||||
|
# Проверка восстановления
|
||||||
|
recoveries = []
|
||||||
|
if system_info['cpu_percent'] < self.recovery_threshold and self.alert_states['cpu']:
|
||||||
|
self.alert_states['cpu'] = False
|
||||||
|
recoveries.append(('cpu', system_info['cpu_percent']))
|
||||||
|
|
||||||
|
if system_info['ram_percent'] < self.recovery_threshold and self.alert_states['ram']:
|
||||||
|
self.alert_states['ram'] = False
|
||||||
|
recoveries.append(('ram', system_info['ram_percent']))
|
||||||
|
|
||||||
|
if system_info['disk_percent'] < self.recovery_threshold and self.alert_states['disk']:
|
||||||
|
self.alert_states['disk'] = False
|
||||||
|
recoveries.append(('disk', system_info['disk_percent']))
|
||||||
|
|
||||||
|
return alerts, recoveries
|
||||||
|
|
||||||
|
async def send_status_message(self, system_info: Dict):
|
||||||
|
"""Отправка сообщения со статусом сервера"""
|
||||||
|
try:
|
||||||
|
voice_bot_status = self.check_process_status('voice_bot')
|
||||||
|
helper_bot_status = self.check_process_status('helper_bot')
|
||||||
|
|
||||||
|
message = f"""🖥 **Статус Сервера** | <code>{system_info['current_time']}</code>
|
||||||
|
---------------------------------
|
||||||
|
**📊 Общая нагрузка:**
|
||||||
|
CPU: <b>{system_info['cpu_percent']}%</b> | LA: <b>{system_info['load_avg_1m']} / {system_info['cpu_count']}</b> | IO Wait: <b>{system_info['disk_percent']}%</b>
|
||||||
|
|
||||||
|
**💾 Память:**
|
||||||
|
RAM: <b>{system_info['ram_used']}/{system_info['ram_total']} GB</b> ({system_info['ram_percent']}%)
|
||||||
|
Swap: <b>{system_info['swap_used']}/{system_info['swap_total']} GB</b> ({system_info['swap_percent']}%)
|
||||||
|
|
||||||
|
**💿 Диск I/O:**
|
||||||
|
Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_write_speed']}</b>
|
||||||
|
Диск загружен: <b>{system_info['disk_io_percent']}%</b>
|
||||||
|
|
||||||
|
**🤖 Процессы:**
|
||||||
|
{voice_bot_status} voice-bot | {helper_bot_status} helper-bot
|
||||||
|
---------------------------------
|
||||||
|
⏰ Uptime: <code>{system_info['system_uptime']}</code>"""
|
||||||
|
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=self.group_for_logs,
|
||||||
|
text=message,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
logger.info("Статус сервера отправлен")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке статуса сервера: {e}")
|
||||||
|
|
||||||
|
async def send_alert_message(self, metric_name: str, current_value: float, details: str):
|
||||||
|
"""Отправка сообщения об алерте"""
|
||||||
|
try:
|
||||||
|
message = f"""🚨 **ALERT: Высокая нагрузка на сервере!**
|
||||||
|
---------------------------------
|
||||||
|
**Показатель:** {metric_name}
|
||||||
|
**Текущее значение:** <b>{current_value}%</b> ⚠️
|
||||||
|
**Пороговое значение:** 80%
|
||||||
|
|
||||||
|
**Детали:**
|
||||||
|
{details}
|
||||||
|
|
||||||
|
**Сервер:** `{psutil.os.uname().nodename}`
|
||||||
|
**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
|
||||||
|
---------------------------------"""
|
||||||
|
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=self.important_logs,
|
||||||
|
text=message,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
logger.warning(f"Алерт отправлен: {metric_name} - {current_value}%")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке алерта: {e}")
|
||||||
|
|
||||||
|
async def send_recovery_message(self, metric_name: str, current_value: float, peak_value: float):
|
||||||
|
"""Отправка сообщения о восстановлении"""
|
||||||
|
try:
|
||||||
|
message = f"""✅ **RECOVERY: Нагрузка нормализовалась**
|
||||||
|
---------------------------------
|
||||||
|
**Показатель:** {metric_name}
|
||||||
|
**Текущее значение:** <b>{current_value}%</b> ✔️
|
||||||
|
**Было превышение:** До {peak_value}%
|
||||||
|
|
||||||
|
**Сервер:** `{psutil.os.uname().nodename}`
|
||||||
|
**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
|
||||||
|
---------------------------------"""
|
||||||
|
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=self.important_logs,
|
||||||
|
text=message,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
logger.info(f"Сообщение о восстановлении отправлено: {metric_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке сообщения о восстановлении: {e}")
|
||||||
|
|
||||||
|
async def monitor_loop(self):
|
||||||
|
"""Основной цикл мониторинга"""
|
||||||
|
logger.info("Модуль мониторинга сервера запущен")
|
||||||
|
|
||||||
|
# Отправляем сообщение о запуске при первом запуске
|
||||||
|
if self.should_send_startup_status():
|
||||||
|
await self.send_startup_message()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
system_info = self.get_system_info()
|
||||||
|
if not system_info:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверка алертов
|
||||||
|
alerts, recoveries = self.check_alerts(system_info)
|
||||||
|
|
||||||
|
# Отправка алертов
|
||||||
|
for metric_type, value, details in alerts:
|
||||||
|
metric_names = {
|
||||||
|
'cpu': 'Использование CPU',
|
||||||
|
'ram': 'Использование оперативной памяти',
|
||||||
|
'disk': 'Заполнение диска (/)'
|
||||||
|
}
|
||||||
|
await self.send_alert_message(metric_names[metric_type], value, details)
|
||||||
|
|
||||||
|
# Отправка сообщений о восстановлении
|
||||||
|
for metric_type, value in recoveries:
|
||||||
|
metric_names = {
|
||||||
|
'cpu': 'Использование CPU',
|
||||||
|
'ram': 'Использование оперативной памяти',
|
||||||
|
'disk': 'Заполнение диска (/)'
|
||||||
|
}
|
||||||
|
# Находим пиковое значение (используем 80% как пример)
|
||||||
|
await self.send_recovery_message(metric_names[metric_type], value, 80.0)
|
||||||
|
|
||||||
|
# Отправка статуса каждые 30 минут в 00 и 30 минут часа
|
||||||
|
if self.should_send_status():
|
||||||
|
await self.send_status_message(system_info)
|
||||||
|
|
||||||
|
# Пауза между проверками (1 минута)
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в цикле мониторинга: {e}")
|
||||||
|
await asyncio.sleep(60)
|
||||||
@@ -4,6 +4,9 @@ aiogram~=3.10.0
|
|||||||
# Logging
|
# Logging
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
|
|
||||||
|
# System monitoring
|
||||||
|
psutil~=6.1.0
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
pytest==8.2.2
|
pytest==8.2.2
|
||||||
pytest-asyncio==1.1.0
|
pytest-asyncio==1.1.0
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import signal
|
||||||
|
|
||||||
# Ensure project root is on sys.path for module resolution
|
# Ensure project root is on sys.path for module resolution
|
||||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -9,6 +10,82 @@ if CURRENT_DIR not in sys.path:
|
|||||||
|
|
||||||
from helper_bot.main import start_bot
|
from helper_bot.main import start_bot
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
from helper_bot.server_monitor import ServerMonitor
|
||||||
|
|
||||||
|
|
||||||
|
async def start_monitoring(bdf, bot):
|
||||||
|
"""Запуск модуля мониторинга сервера"""
|
||||||
|
monitor = ServerMonitor(
|
||||||
|
bot=bot,
|
||||||
|
group_for_logs=bdf.settings['Telegram']['group_for_logs'],
|
||||||
|
important_logs=bdf.settings['Telegram']['important_logs']
|
||||||
|
)
|
||||||
|
return monitor
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Основная функция запуска"""
|
||||||
|
bdf = get_global_instance()
|
||||||
|
|
||||||
|
# Создаем бота для мониторинга
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
|
monitor_bot = Bot(
|
||||||
|
token=bdf.settings['Telegram']['bot_token'],
|
||||||
|
default=DefaultBotProperties(parse_mode='HTML'),
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем экземпляр монитора
|
||||||
|
monitor = await start_monitoring(bdf, monitor_bot)
|
||||||
|
|
||||||
|
# Флаг для корректного завершения
|
||||||
|
shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""Обработчик сигналов для корректного завершения"""
|
||||||
|
print(f"\nПолучен сигнал {signum}, завершаем работу...")
|
||||||
|
shutdown_event.set()
|
||||||
|
|
||||||
|
# Регистрируем обработчики сигналов
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
# Запускаем бота и мониторинг
|
||||||
|
bot_task = asyncio.create_task(start_bot(bdf))
|
||||||
|
monitor_task = asyncio.create_task(monitor.monitor_loop())
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ждем сигнала завершения
|
||||||
|
await shutdown_event.wait()
|
||||||
|
print("Начинаем корректное завершение...")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Получен сигнал завершения...")
|
||||||
|
finally:
|
||||||
|
print("Отправляем сообщение об отключении...")
|
||||||
|
try:
|
||||||
|
# Отправляем сообщение об отключении
|
||||||
|
await monitor.send_shutdown_message()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при отправке сообщения об отключении: {e}")
|
||||||
|
|
||||||
|
print("Останавливаем задачи...")
|
||||||
|
# Отменяем задачи
|
||||||
|
bot_task.cancel()
|
||||||
|
monitor_task.cancel()
|
||||||
|
|
||||||
|
# Ждем завершения задач
|
||||||
|
try:
|
||||||
|
await asyncio.gather(bot_task, monitor_task, return_exceptions=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при остановке задач: {e}")
|
||||||
|
|
||||||
|
# Закрываем сессию бота
|
||||||
|
await monitor_bot.session.close()
|
||||||
|
print("Бот корректно остановлен")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(start_bot(get_global_instance()))
|
asyncio.run(main())
|
||||||
|
|||||||
81
tests/test_monitor.py
Normal file
81
tests/test_monitor.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тестовый скрипт для проверки модуля мониторинга сервера
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from helper_bot.server_monitor import ServerMonitor
|
||||||
|
|
||||||
|
|
||||||
|
class MockBot:
|
||||||
|
"""Мок объект бота для тестирования"""
|
||||||
|
|
||||||
|
async def send_message(self, chat_id, text, parse_mode=None):
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Отправка в чат: {chat_id}")
|
||||||
|
print(f"Текст сообщения:")
|
||||||
|
print(text)
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_monitor():
|
||||||
|
"""Тестирование модуля мониторинга"""
|
||||||
|
print("🧪 Тестирование модуля мониторинга сервера")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Создаем мок бота
|
||||||
|
mock_bot = MockBot()
|
||||||
|
|
||||||
|
# Создаем монитор
|
||||||
|
monitor = ServerMonitor(
|
||||||
|
bot=mock_bot,
|
||||||
|
group_for_logs="-123456789",
|
||||||
|
important_logs="-987654321"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("📊 Получение информации о системе...")
|
||||||
|
system_info = monitor.get_system_info()
|
||||||
|
|
||||||
|
if system_info:
|
||||||
|
print("✅ Информация о системе получена успешно")
|
||||||
|
print(f"CPU: {system_info['cpu_percent']}%")
|
||||||
|
print(f"RAM: {system_info['ram_percent']}%")
|
||||||
|
print(f"Disk: {system_info['disk_percent']}%")
|
||||||
|
print(f"Uptime: {system_info['system_uptime']}")
|
||||||
|
|
||||||
|
print("\n🤖 Проверка статуса процессов...")
|
||||||
|
voice_status = monitor.check_process_status('voice_bot')
|
||||||
|
helper_status = monitor.check_process_status('helper_bot')
|
||||||
|
print(f"Voice Bot: {voice_status}")
|
||||||
|
print(f"Helper Bot: {helper_status}")
|
||||||
|
|
||||||
|
print("\n📝 Тестирование отправки статуса...")
|
||||||
|
await monitor.send_status_message(system_info)
|
||||||
|
|
||||||
|
print("\n🚨 Тестирование отправки алерта...")
|
||||||
|
await monitor.send_alert_message(
|
||||||
|
"Использование CPU",
|
||||||
|
85.5,
|
||||||
|
"Нагрузка за 1 мин: 2.5"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n✅ Тестирование отправки сообщения о восстановлении...")
|
||||||
|
await monitor.send_recovery_message(
|
||||||
|
"Использование CPU",
|
||||||
|
70.0,
|
||||||
|
85.5
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Не удалось получить информацию о системе")
|
||||||
|
|
||||||
|
print("\n🎯 Тестирование завершено!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_monitor())
|
||||||
Reference in New Issue
Block a user