From dc0e5d788c0f3fc3e1034f3171b4d6fa21606528 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 27 Aug 2025 20:09:48 +0300 Subject: [PATCH] Implement OS detection and enhance disk monitoring in ServerMonitor - Added OS detection functionality to the ServerMonitor class, allowing for tailored disk usage and uptime calculations based on the operating system (macOS or Ubuntu). - Introduced methods for retrieving disk usage and I/O statistics specific to the detected OS. - Updated the process status check to return uptime information for monitored processes. - Enhanced the status message format to include disk space emojis and process uptime details. - Updated tests to reflect changes in process status checks and output formatting. --- helper_bot/server_monitor.py | 212 +++++++++++++++++++++++++++++++---- tests/test_monitor.py | 8 +- 2 files changed, 195 insertions(+), 25 deletions(-) diff --git a/helper_bot/server_monitor.py b/helper_bot/server_monitor.py index d43eafb..d568b2c 100644 --- a/helper_bot/server_monitor.py +++ b/helper_bot/server_monitor.py @@ -2,6 +2,7 @@ import asyncio import os import psutil import time +import platform from datetime import datetime, timedelta from typing import Dict, Optional, Tuple import logging @@ -15,6 +16,10 @@ class ServerMonitor: self.group_for_logs = group_for_logs self.important_logs = important_logs + # Определяем ОС + self.os_type = self._detect_os() + logger.info(f"Обнаружена ОС: {self.os_type}") + # Пороговые значения для алертов self.threshold = 80.0 self.recovery_threshold = 75.0 @@ -39,6 +44,119 @@ class ServerMonitor: self.last_disk_io = None self.last_disk_io_time = None + # Время запуска бота для расчета uptime + self.bot_start_time = time.time() + + def _detect_os(self) -> str: + """Определение типа операционной системы""" + system = platform.system().lower() + if system == "darwin": + return "macos" + elif system == "linux": + return "ubuntu" + else: + return "unknown" + + def _get_disk_path(self) -> str: + """Получение пути к диску в зависимости от ОС""" + if self.os_type == "macos": + return "/" + elif self.os_type == "ubuntu": + return "/" + else: + return "/" + + def _get_disk_usage(self) -> Optional[object]: + """Получение информации о диске с учетом ОС""" + try: + if self.os_type == "macos": + # На macOS используем diskutil для получения реального использования диска + return self._get_macos_disk_usage() + else: + disk_path = self._get_disk_path() + return psutil.disk_usage(disk_path) + except Exception as e: + logger.error(f"Ошибка при получении информации о диске: {e}") + return None + + def _get_macos_disk_usage(self) -> Optional[object]: + """Получение информации о диске на macOS через diskutil""" + try: + import subprocess + import re + + # Получаем информацию о диске через diskutil + result = subprocess.run(['diskutil', 'info', '/'], capture_output=True, text=True) + if result.returncode != 0: + # Fallback к psutil + return psutil.disk_usage('/') + + output = result.stdout + + # Извлекаем размеры из вывода diskutil + total_match = re.search(r'Container Total Space:\s+(\d+\.\d+)\s+GB', output) + free_match = re.search(r'Container Free Space:\s+(\d+\.\d+)\s+GB', output) + + if total_match and free_match: + total_gb = float(total_match.group(1)) + free_gb = float(free_match.group(1)) + used_gb = total_gb - free_gb + + # Создаем объект, похожий на результат psutil.disk_usage + class DiskUsage: + def __init__(self, total, used, free): + self.total = total * (1024**3) # Конвертируем в байты + self.used = used * (1024**3) + self.free = free * (1024**3) + + return DiskUsage(total_gb, used_gb, free_gb) + else: + # Fallback к psutil + return psutil.disk_usage('/') + + except Exception as e: + logger.error(f"Ошибка при получении информации о диске macOS: {e}") + # Fallback к psutil + return psutil.disk_usage('/') + + def _get_disk_io_counters(self): + """Получение статистики диска с учетом ОС""" + try: + if self.os_type == "macos": + # На macOS может быть несколько дисков, берем основной + return psutil.disk_io_counters(perdisk=False) + elif self.os_type == "ubuntu": + # На Ubuntu обычно один диск + return psutil.disk_io_counters(perdisk=False) + else: + return psutil.disk_io_counters() + except Exception as e: + logger.error(f"Ошибка при получении статистики диска: {e}") + return None + + def _get_system_uptime(self) -> float: + """Получение uptime системы с учетом ОС""" + try: + if self.os_type == "macos": + # На macOS используем boot_time + boot_time = psutil.boot_time() + return time.time() - boot_time + elif self.os_type == "ubuntu": + # На Ubuntu также используем boot_time + boot_time = psutil.boot_time() + return time.time() - boot_time + else: + boot_time = psutil.boot_time() + return time.time() - boot_time + except Exception as e: + logger.error(f"Ошибка при получении uptime системы: {e}") + return 0.0 + + def get_bot_uptime(self) -> str: + """Получение uptime бота""" + uptime_seconds = time.time() - self.bot_start_time + return self._format_uptime(uptime_seconds) + def get_system_info(self) -> Dict: """Получение информации о системе""" try: @@ -51,16 +169,31 @@ class ServerMonitor: memory = psutil.virtual_memory() swap = psutil.swap_memory() + # Используем единый расчет для всех ОС: used / total для получения процента занятой памяти + # Это обеспечивает консистентность между macOS и Ubuntu + ram_percent = (memory.used / memory.total) * 100 + # Диск - disk = psutil.disk_usage('/') - disk_io = psutil.disk_io_counters() + disk = self._get_disk_usage() + disk_io = self._get_disk_io_counters() + + if disk is None: + logger.error("Не удалось получить информацию о диске") + return {} # Расчет скорости диска disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io) # Система - boot_time = psutil.boot_time() - uptime = time.time() - boot_time + system_uptime = self._get_system_uptime() + + # Получаем имя хоста в зависимости от ОС + if self.os_type == "macos": + hostname = os.uname().nodename + elif self.os_type == "ubuntu": + hostname = os.uname().nodename + else: + hostname = "unknown" return { 'cpu_percent': cpu_percent, @@ -70,25 +203,35 @@ class ServerMonitor: '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), + 'ram_percent': round(ram_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_percent': round((disk.used / disk.total) * 100, 1), '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, + 'system_uptime': self._format_uptime(system_uptime), + 'bot_uptime': self.get_bot_uptime(), + 'server_hostname': hostname, 'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } except Exception as e: logger.error(f"Ошибка при получении информации о системе: {e}") return {} + def _get_disk_space_emoji(self, disk_percent: float) -> str: + """Получение эмодзи для дискового пространства""" + if disk_percent < 60: + return "🟢" + elif disk_percent < 90: + return "⚠️" + else: + return "🚨" + def _format_bytes(self, bytes_value: int) -> str: """Форматирование байтов в человекочитаемый вид""" if bytes_value == 0: @@ -115,8 +258,8 @@ class ServerMonitor: else: return f"{minutes}м" - def check_process_status(self, process_name: str) -> str: - """Проверка статуса процесса""" + def check_process_status(self, process_name: str) -> Tuple[str, str]: + """Проверка статуса процесса и возврат статуса с uptime""" try: # Сначала проверяем по PID файлу pid_file = self.pid_files.get(process_name) @@ -127,7 +270,14 @@ class ServerMonitor: if content and content != '# Этот файл будет автоматически обновляться при запуске бота': pid = int(content) if psutil.pid_exists(pid): - return "✅" + # Получаем uptime процесса + try: + proc = psutil.Process(pid) + proc_uptime = time.time() - proc.create_time() + uptime_str = self._format_uptime(proc_uptime) + return "✅", f"Uptime {uptime_str}" + except: + return "✅", "Uptime неизвестно" except (ValueError, FileNotFoundError): pass @@ -143,21 +293,33 @@ class ServerMonitor: if ('voice_bot' in proc_name or 'voice_bot' in cmdline or 'voice_bot_v2.py' in cmdline): - return "✅" + # Получаем uptime процесса + try: + proc_uptime = time.time() - proc.create_time() + uptime_str = self._format_uptime(proc_uptime) + return "✅", f"Uptime {uptime_str}" + except: + return "✅", "Uptime неизвестно" 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 "✅" + # Получаем uptime процесса + try: + proc_uptime = time.time() - proc.create_time() + uptime_str = self._format_uptime(proc_uptime) + return "✅", f"Uptime {uptime_str}" + except: + return "✅", "Uptime неизвестно" except (psutil.NoSuchProcess, psutil.AccessDenied): continue - return "❌" + return "❌", "Выключен" except Exception as e: logger.error(f"Ошибка при проверке процесса {process_name}: {e}") - return "❌" + return "❌", "Выключен" def should_send_status(self) -> bool: """Проверка, нужно ли отправить статус (каждые 30 минут в 00 и 30 минут часа)""" @@ -203,7 +365,7 @@ class ServerMonitor: """Расчет процента загрузки диска на основе IOPS""" try: # Получаем статистику диска - disk_io = psutil.disk_io_counters() + disk_io = self._get_disk_io_counters() if disk_io is None: return 0 @@ -237,6 +399,7 @@ class ServerMonitor: **Время запуска:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} **Сервер:** `{psutil.os.uname().nodename}` **Система:** {psutil.os.uname().sysname} {psutil.os.uname().release} +**ОС:** {self.os_type.upper()} ✅ Мониторинг сервера активирован ✅ Статус будет отправляться каждые 30 минут (в 00 и 30 минут часа) @@ -324,8 +487,11 @@ class ServerMonitor: 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') + voice_bot_status, voice_bot_uptime = self.check_process_status('voice_bot') + helper_bot_status, helper_bot_uptime = self.check_process_status('helper_bot') + + # Получаем эмодзи для дискового пространства + disk_emoji = self._get_disk_space_emoji(system_info['disk_percent']) message = f"""🖥 **Статус Сервера** | {system_info['current_time']} --------------------------------- @@ -336,14 +502,18 @@ CPU: {system_info['cpu_percent']}% | LA: {system_info['load_avg_1m']} RAM: {system_info['ram_used']}/{system_info['ram_total']} GB ({system_info['ram_percent']}%) Swap: {system_info['swap_used']}/{system_info['swap_total']} GB ({system_info['swap_percent']}%) +**🗂️ Дисковое пространство:** +Диск (/): {system_info['disk_used']}/{system_info['disk_total']} GB ({system_info['disk_percent']}%) {disk_emoji} + **💿 Диск I/O:** Read: {system_info['disk_read_speed']} | Write: {system_info['disk_write_speed']} Диск загружен: {system_info['disk_io_percent']}% **🤖 Процессы:** -{voice_bot_status} voice-bot | {helper_bot_status} helper-bot +{voice_bot_status} voice-bot - {voice_bot_uptime} +{helper_bot_status} helper-bot - {helper_bot_uptime} --------------------------------- -⏰ Uptime: {system_info['system_uptime']}""" +⏰ Uptime сервера: {system_info['system_uptime']}""" await self.bot.send_message( chat_id=self.group_for_logs, @@ -406,7 +576,7 @@ Read: {system_info['disk_read_speed']} | Write: {system_info['disk_wri async def monitor_loop(self): """Основной цикл мониторинга""" - logger.info("Модуль мониторинга сервера запущен") + logger.info(f"Модуль мониторинга сервера запущен на {self.os_type.upper()}") # Отправляем сообщение о запуске при первом запуске if self.should_send_startup_status(): diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 130c649..b4d3259 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -49,10 +49,10 @@ async def test_monitor(): 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}") + voice_status, voice_uptime = monitor.check_process_status('voice_bot') + helper_status, helper_uptime = monitor.check_process_status('helper_bot') + print(f"Voice Bot: {voice_status} - {voice_uptime}") + print(f"Helper Bot: {helper_status} - {helper_uptime}") print("\n📝 Тестирование отправки статуса...") await monitor.send_status_message(system_info)