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.
This commit is contained in:
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
import time
|
import time
|
||||||
|
import platform
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
import logging
|
import logging
|
||||||
@@ -15,6 +16,10 @@ class ServerMonitor:
|
|||||||
self.group_for_logs = group_for_logs
|
self.group_for_logs = group_for_logs
|
||||||
self.important_logs = important_logs
|
self.important_logs = important_logs
|
||||||
|
|
||||||
|
# Определяем ОС
|
||||||
|
self.os_type = self._detect_os()
|
||||||
|
logger.info(f"Обнаружена ОС: {self.os_type}")
|
||||||
|
|
||||||
# Пороговые значения для алертов
|
# Пороговые значения для алертов
|
||||||
self.threshold = 80.0
|
self.threshold = 80.0
|
||||||
self.recovery_threshold = 75.0
|
self.recovery_threshold = 75.0
|
||||||
@@ -39,6 +44,119 @@ class ServerMonitor:
|
|||||||
self.last_disk_io = None
|
self.last_disk_io = None
|
||||||
self.last_disk_io_time = 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:
|
def get_system_info(self) -> Dict:
|
||||||
"""Получение информации о системе"""
|
"""Получение информации о системе"""
|
||||||
try:
|
try:
|
||||||
@@ -51,16 +169,31 @@ class ServerMonitor:
|
|||||||
memory = psutil.virtual_memory()
|
memory = psutil.virtual_memory()
|
||||||
swap = psutil.swap_memory()
|
swap = psutil.swap_memory()
|
||||||
|
|
||||||
|
# Используем единый расчет для всех ОС: used / total для получения процента занятой памяти
|
||||||
|
# Это обеспечивает консистентность между macOS и Ubuntu
|
||||||
|
ram_percent = (memory.used / memory.total) * 100
|
||||||
|
|
||||||
# Диск
|
# Диск
|
||||||
disk = psutil.disk_usage('/')
|
disk = self._get_disk_usage()
|
||||||
disk_io = psutil.disk_io_counters()
|
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)
|
disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io)
|
||||||
|
|
||||||
# Система
|
# Система
|
||||||
boot_time = psutil.boot_time()
|
system_uptime = self._get_system_uptime()
|
||||||
uptime = time.time() - boot_time
|
|
||||||
|
# Получаем имя хоста в зависимости от ОС
|
||||||
|
if self.os_type == "macos":
|
||||||
|
hostname = os.uname().nodename
|
||||||
|
elif self.os_type == "ubuntu":
|
||||||
|
hostname = os.uname().nodename
|
||||||
|
else:
|
||||||
|
hostname = "unknown"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'cpu_percent': cpu_percent,
|
'cpu_percent': cpu_percent,
|
||||||
@@ -70,25 +203,35 @@ class ServerMonitor:
|
|||||||
'cpu_count': cpu_count,
|
'cpu_count': cpu_count,
|
||||||
'ram_used': round(memory.used / (1024**3), 2),
|
'ram_used': round(memory.used / (1024**3), 2),
|
||||||
'ram_total': round(memory.total / (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_used': round(swap.used / (1024**3), 2),
|
||||||
'swap_total': round(swap.total / (1024**3), 2),
|
'swap_total': round(swap.total / (1024**3), 2),
|
||||||
'swap_percent': swap.percent,
|
'swap_percent': swap.percent,
|
||||||
'disk_used': round(disk.used / (1024**3), 2),
|
'disk_used': round(disk.used / (1024**3), 2),
|
||||||
'disk_total': round(disk.total / (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_free': round(disk.free / (1024**3), 2),
|
||||||
'disk_read_speed': disk_read_speed,
|
'disk_read_speed': disk_read_speed,
|
||||||
'disk_write_speed': disk_write_speed,
|
'disk_write_speed': disk_write_speed,
|
||||||
'disk_io_percent': self._calculate_disk_io_percent(),
|
'disk_io_percent': self._calculate_disk_io_percent(),
|
||||||
'system_uptime': self._format_uptime(uptime),
|
'system_uptime': self._format_uptime(system_uptime),
|
||||||
'server_hostname': psutil.os.uname().nodename,
|
'bot_uptime': self.get_bot_uptime(),
|
||||||
|
'server_hostname': hostname,
|
||||||
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении информации о системе: {e}")
|
logger.error(f"Ошибка при получении информации о системе: {e}")
|
||||||
return {}
|
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:
|
def _format_bytes(self, bytes_value: int) -> str:
|
||||||
"""Форматирование байтов в человекочитаемый вид"""
|
"""Форматирование байтов в человекочитаемый вид"""
|
||||||
if bytes_value == 0:
|
if bytes_value == 0:
|
||||||
@@ -115,8 +258,8 @@ class ServerMonitor:
|
|||||||
else:
|
else:
|
||||||
return f"{minutes}м"
|
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:
|
try:
|
||||||
# Сначала проверяем по PID файлу
|
# Сначала проверяем по PID файлу
|
||||||
pid_file = self.pid_files.get(process_name)
|
pid_file = self.pid_files.get(process_name)
|
||||||
@@ -127,7 +270,14 @@ class ServerMonitor:
|
|||||||
if content and content != '# Этот файл будет автоматически обновляться при запуске бота':
|
if content and content != '# Этот файл будет автоматически обновляться при запуске бота':
|
||||||
pid = int(content)
|
pid = int(content)
|
||||||
if psutil.pid_exists(pid):
|
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):
|
except (ValueError, FileNotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -143,21 +293,33 @@ class ServerMonitor:
|
|||||||
if ('voice_bot' in proc_name or
|
if ('voice_bot' in proc_name or
|
||||||
'voice_bot' in cmdline or
|
'voice_bot' in cmdline or
|
||||||
'voice_bot_v2.py' in cmdline):
|
'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':
|
elif process_name == 'helper_bot':
|
||||||
# Проверяем helper_bot
|
# Проверяем helper_bot
|
||||||
if ('helper_bot' in proc_name or
|
if ('helper_bot' in proc_name or
|
||||||
'helper_bot' in cmdline or
|
'helper_bot' in cmdline or
|
||||||
'run_helper.py' in cmdline or
|
'run_helper.py' in cmdline or
|
||||||
'python' in proc_name and 'helper_bot' in cmdline):
|
'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):
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return "❌"
|
return "❌", "Выключен"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при проверке процесса {process_name}: {e}")
|
logger.error(f"Ошибка при проверке процесса {process_name}: {e}")
|
||||||
return "❌"
|
return "❌", "Выключен"
|
||||||
|
|
||||||
def should_send_status(self) -> bool:
|
def should_send_status(self) -> bool:
|
||||||
"""Проверка, нужно ли отправить статус (каждые 30 минут в 00 и 30 минут часа)"""
|
"""Проверка, нужно ли отправить статус (каждые 30 минут в 00 и 30 минут часа)"""
|
||||||
@@ -203,7 +365,7 @@ class ServerMonitor:
|
|||||||
"""Расчет процента загрузки диска на основе IOPS"""
|
"""Расчет процента загрузки диска на основе IOPS"""
|
||||||
try:
|
try:
|
||||||
# Получаем статистику диска
|
# Получаем статистику диска
|
||||||
disk_io = psutil.disk_io_counters()
|
disk_io = self._get_disk_io_counters()
|
||||||
if disk_io is None:
|
if disk_io is None:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -237,6 +399,7 @@ class ServerMonitor:
|
|||||||
**Время запуска:** <code>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</code>
|
**Время запуска:** <code>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</code>
|
||||||
**Сервер:** `{psutil.os.uname().nodename}`
|
**Сервер:** `{psutil.os.uname().nodename}`
|
||||||
**Система:** {psutil.os.uname().sysname} {psutil.os.uname().release}
|
**Система:** {psutil.os.uname().sysname} {psutil.os.uname().release}
|
||||||
|
**ОС:** {self.os_type.upper()}
|
||||||
|
|
||||||
✅ Мониторинг сервера активирован
|
✅ Мониторинг сервера активирован
|
||||||
✅ Статус будет отправляться каждые 30 минут (в 00 и 30 минут часа)
|
✅ Статус будет отправляться каждые 30 минут (в 00 и 30 минут часа)
|
||||||
@@ -324,8 +487,11 @@ class ServerMonitor:
|
|||||||
async def send_status_message(self, system_info: Dict):
|
async def send_status_message(self, system_info: Dict):
|
||||||
"""Отправка сообщения со статусом сервера"""
|
"""Отправка сообщения со статусом сервера"""
|
||||||
try:
|
try:
|
||||||
voice_bot_status = self.check_process_status('voice_bot')
|
voice_bot_status, voice_bot_uptime = self.check_process_status('voice_bot')
|
||||||
helper_bot_status = self.check_process_status('helper_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"""🖥 **Статус Сервера** | <code>{system_info['current_time']}</code>
|
message = f"""🖥 **Статус Сервера** | <code>{system_info['current_time']}</code>
|
||||||
---------------------------------
|
---------------------------------
|
||||||
@@ -336,14 +502,18 @@ CPU: <b>{system_info['cpu_percent']}%</b> | LA: <b>{system_info['load_avg_1m']}
|
|||||||
RAM: <b>{system_info['ram_used']}/{system_info['ram_total']} GB</b> ({system_info['ram_percent']}%)
|
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']}%)
|
Swap: <b>{system_info['swap_used']}/{system_info['swap_total']} GB</b> ({system_info['swap_percent']}%)
|
||||||
|
|
||||||
|
**🗂️ Дисковое пространство:**
|
||||||
|
Диск (/): <b>{system_info['disk_used']}/{system_info['disk_total']} GB</b> ({system_info['disk_percent']}%) {disk_emoji}
|
||||||
|
|
||||||
**💿 Диск I/O:**
|
**💿 Диск I/O:**
|
||||||
Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_write_speed']}</b>
|
Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_write_speed']}</b>
|
||||||
Диск загружен: <b>{system_info['disk_io_percent']}%</b>
|
Диск загружен: <b>{system_info['disk_io_percent']}%</b>
|
||||||
|
|
||||||
**🤖 Процессы:**
|
**🤖 Процессы:**
|
||||||
{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: <code>{system_info['system_uptime']}</code>"""
|
⏰ Uptime сервера: {system_info['system_uptime']}"""
|
||||||
|
|
||||||
await self.bot.send_message(
|
await self.bot.send_message(
|
||||||
chat_id=self.group_for_logs,
|
chat_id=self.group_for_logs,
|
||||||
@@ -406,7 +576,7 @@ Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_wri
|
|||||||
|
|
||||||
async def monitor_loop(self):
|
async def monitor_loop(self):
|
||||||
"""Основной цикл мониторинга"""
|
"""Основной цикл мониторинга"""
|
||||||
logger.info("Модуль мониторинга сервера запущен")
|
logger.info(f"Модуль мониторинга сервера запущен на {self.os_type.upper()}")
|
||||||
|
|
||||||
# Отправляем сообщение о запуске при первом запуске
|
# Отправляем сообщение о запуске при первом запуске
|
||||||
if self.should_send_startup_status():
|
if self.should_send_startup_status():
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ async def test_monitor():
|
|||||||
print(f"Uptime: {system_info['system_uptime']}")
|
print(f"Uptime: {system_info['system_uptime']}")
|
||||||
|
|
||||||
print("\n🤖 Проверка статуса процессов...")
|
print("\n🤖 Проверка статуса процессов...")
|
||||||
voice_status = monitor.check_process_status('voice_bot')
|
voice_status, voice_uptime = monitor.check_process_status('voice_bot')
|
||||||
helper_status = monitor.check_process_status('helper_bot')
|
helper_status, helper_uptime = monitor.check_process_status('helper_bot')
|
||||||
print(f"Voice Bot: {voice_status}")
|
print(f"Voice Bot: {voice_status} - {voice_uptime}")
|
||||||
print(f"Helper Bot: {helper_status}")
|
print(f"Helper Bot: {helper_status} - {helper_uptime}")
|
||||||
|
|
||||||
print("\n📝 Тестирование отправки статуса...")
|
print("\n📝 Тестирование отправки статуса...")
|
||||||
await monitor.send_status_message(system_info)
|
await monitor.send_status_message(system_info)
|
||||||
|
|||||||
Reference in New Issue
Block a user