Files
prod/infra/monitoring/metrics_collector.py

850 lines
43 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import psutil
import time
import platform
from datetime import datetime
from typing import Dict, Optional, Tuple
import logging
from pid_manager import create_pid_manager
logger = logging.getLogger(__name__)
class MetricsCollector:
def __init__(self):
# Определяем ОС
self.os_type = self._detect_os()
logger.info(f"Обнаружена ОС: {self.os_type}")
# Проверяем, запущены ли мы в Docker с доступом к хосту
self.is_docker_host_monitoring = self._check_docker_host_access()
if self.is_docker_host_monitoring:
logger.info("Обнаружен доступ к хосту через Docker volumes - мониторинг будет вестись на уровне хоста")
else:
logger.warning("Мониторинг будет вестись на уровне контейнера (не рекомендуется для продакшена)")
# Пороговые значения для алертов
self.threshold = float(os.getenv('THRESHOLD', '80.0'))
self.recovery_threshold = float(os.getenv('RECOVERY_THRESHOLD', '75.0'))
# Задержки для алертов (в секундах) - предотвращают ложные срабатывания
self.alert_delays = {
'cpu': int(os.getenv('CPU_ALERT_DELAY', '30')), # 30 сек для CPU
'ram': int(os.getenv('RAM_ALERT_DELAY', '45')), # 45 сек для RAM
'disk': int(os.getenv('DISK_ALERT_DELAY', '60')) # 60 сек для диска
}
# Состояние алертов для предотвращения спама
self.alert_states = {
'cpu': False,
'ram': False,
'disk': False
}
# Время первого превышения порога для каждого метрика
self.alert_start_times = {
'cpu': None,
'ram': None,
'disk': None
}
# PID файлы для отслеживания процессов
# Определяем корень проекта для поиска PID файлов
current_file = os.path.abspath(__file__)
self.project_root = os.path.dirname(os.path.dirname(current_file))
self.pid_files = {
'helper_bot': os.path.join(self.project_root, 'helper_bot.pid')
}
# Для расчета скорости диска
self.last_disk_io = None
self.last_disk_io_time = None
# Для расчета процента загрузки диска (отдельные переменные)
self.last_disk_io_for_percent = None
self.last_disk_io_time_for_percent = None
# Инициализируем базовые значения для скорости диска при первом вызове
self._initialize_disk_io()
# Время запуска мониторинга для расчета uptime
self.monitor_start_time = time.time()
logger.info(f"Инициализированы задержки алертов: CPU={self.alert_delays['cpu']}s, RAM={self.alert_delays['ram']}s, Disk={self.alert_delays['disk']}s")
def add_bot_to_monitoring(self, bot_name: str):
"""
Добавление нового бота в мониторинг
Args:
bot_name: Имя бота (например, 'helper_bot', 'admin_bot', etc.)
"""
pid_file_path = os.path.join(self.project_root, f"{bot_name}.pid")
self.pid_files[bot_name] = pid_file_path
logger.info(f"Добавлен бот {bot_name} в мониторинг: {pid_file_path}")
def _detect_os(self) -> str:
"""Определение типа операционной системы"""
system = platform.system().lower()
if system == "darwin":
return "macos"
elif system == "linux":
return "ubuntu"
else:
return "unknown"
def _check_docker_host_access(self) -> bool:
"""Проверка доступности хоста через Docker volumes"""
try:
# Проверяем, доступны ли файлы хоста через /host/proc
# Это означает, что контейнер запущен с --privileged и volume mounts
if os.path.exists('/host/proc/stat') and os.path.exists('/host/proc/meminfo'):
return True
# Альтернативная проверка - проверяем, запущены ли мы в Docker
# и есть ли доступ к системным файлам хоста
if os.path.exists('/.dockerenv'):
# Проверяем, можем ли мы читать системные файлы хоста
try:
with open('/proc/stat', 'r') as f:
f.read(100) # Читаем немного для проверки доступа
return True
except (OSError, PermissionError):
pass
return False
except Exception as e:
logger.debug(f"Ошибка при проверке доступа к хосту: {e}")
return False
def _initialize_disk_io(self):
"""Инициализация базовых значений для расчета скорости диска"""
try:
disk_io = self._get_disk_io_counters()
if disk_io:
self.last_disk_io = disk_io
self.last_disk_io_time = time.time()
logger.debug("Инициализированы базовые значения для расчета скорости диска")
except Exception as e:
logger.error(f"Ошибка при инициализации диска I/O: {e}")
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_monitor_uptime(self) -> str:
"""Получение uptime мониторинга"""
uptime_seconds = time.time() - self.monitor_start_time
return self._format_uptime(uptime_seconds)
def get_system_info(self) -> Dict:
"""Получение информации о системе"""
try:
# Определяем, какой psutil использовать
current_psutil = psutil
if self.is_docker_host_monitoring:
# Для хоста используем специальные методы
host_cpu = self._get_host_cpu_info()
host_memory = self._get_host_memory_info()
host_disk = self._get_host_disk_info()
if host_cpu and host_memory and host_disk:
# Используем данные хоста
cpu_count = host_cpu['cpu_count']
load_avg = host_cpu['load_avg']
# Для CPU процента используем упрощенный расчет на основе load average
# Load average > 1.0 на ядро считается высокой нагрузкой
load_per_core = load_avg[0] / cpu_count if cpu_count > 0 else 0
cpu_percent = min(100, load_per_core * 100) # Упрощенный расчет
# Память хоста
ram_total = host_memory['ram_total']
ram_used = host_memory['ram_used']
ram_percent = host_memory['ram_percent']
swap_total = host_memory['swap_total']
swap_used = host_memory['swap_used']
swap_percent = host_memory['swap_percent']
# Диск хоста
disk_total = host_disk['total']
disk_used = host_disk['used']
disk_free = host_disk['free']
disk_percent = host_disk['percent']
# IO Wait и другие метрики недоступны через /proc, используем 0
io_wait_percent = 0.0
logger.debug("Используются метрики хоста через Docker volumes")
else:
# Fallback к стандартному psutil
logger.warning("Не удалось получить метрики хоста, используем контейнер")
current_psutil = psutil
host_cpu = host_memory = host_disk = None
else:
# Стандартный psutil для контейнера
host_cpu = host_memory = host_disk = None
# Если не используем хост, получаем стандартные метрики
if not host_cpu:
cpu_percent = current_psutil.cpu_percent(interval=1)
load_avg = current_psutil.getloadavg()
cpu_count = current_psutil.cpu_count()
# CPU times для получения IO Wait
cpu_times = current_psutil.cpu_times_percent(interval=1)
io_wait_percent = getattr(cpu_times, 'iowait', 0.0)
# Память
memory = current_psutil.virtual_memory()
swap = current_psutil.swap_memory()
# Используем единый расчет для всех ОС: used / total для получения процента занятой памяти
ram_percent = (memory.used / memory.total) * 100
ram_total = memory.total
ram_used = memory.used
swap_total = swap.total
swap_used = swap.used
swap_percent = swap.percent
# Диск
disk = self._get_disk_usage()
disk_total = disk.total if disk else 0
disk_used = disk.used if disk else 0
disk_free = disk.free if disk else 0
disk_percent = (disk_used / disk_total * 100) if disk_total > 0 else 0
# Диск I/O (может быть недоступен для хоста)
disk_io = self._get_disk_io_counters()
if disk_io:
disk_io_percent = self._calculate_disk_io_percent()
disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io)
else:
disk_io_percent = 0
disk_read_speed = "0 B/s"
disk_write_speed = "0 B/s"
# Система
system_uptime = self._get_system_uptime()
# Получаем имя хоста
if self.is_docker_host_monitoring:
try:
with open('/host/proc/sys/kernel/hostname', 'r') as f:
hostname = f.read().strip()
except:
hostname = "host"
else:
hostname = os.uname().nodename
return {
'cpu_percent': round(cpu_percent, 1),
'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,
'io_wait_percent': round(io_wait_percent, 1),
'ram_used': round(ram_used / (1024**3), 2),
'ram_total': round(ram_total / (1024**3), 2),
'ram_percent': round(ram_percent, 1),
'swap_used': round(swap_used / (1024**3), 2),
'swap_total': round(swap_total / (1024**3), 2),
'swap_percent': round(swap_percent, 1),
'disk_used': round(disk_used / (1024**3), 2),
'disk_total': round(disk_total / (1024**3), 2),
'disk_percent': round(disk_percent, 1),
'disk_free': round(disk_free / (1024**3), 2),
'disk_read_speed': disk_read_speed,
'disk_write_speed': disk_write_speed,
'disk_io_percent': disk_io_percent,
'system_uptime': self._format_uptime(system_uptime),
'monitor_uptime': self.get_monitor_uptime(),
'server_hostname': hostname,
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'monitoring_level': 'host' if self.is_docker_host_monitoring else 'container'
}
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) -> Tuple[str, str]:
"""Проверка статуса процесса и возврат статуса с uptime"""
try:
# Для helper_bot используем HTTP endpoint
if process_name == 'helper_bot':
return self._check_helper_bot_status()
# Для других процессов используем стандартную проверку
return self._check_local_process_status(process_name)
except Exception as e:
logger.error(f"Ошибка при проверке процесса {process_name}: {e}")
return "", "Выключен"
def _check_local_process_status(self, process_name: str) -> Tuple[str, str]:
"""Проверка локального процесса по PID файлу или имени"""
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):
proc = psutil.Process(pid)
proc_uptime = time.time() - proc.create_time()
uptime_str = self._format_uptime(proc_uptime)
return "", f"Uptime {uptime_str}"
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 in proc_name or
process_name in cmdline or
'python' in proc_name and process_name in cmdline):
proc_uptime = time.time() - proc.create_time()
uptime_str = self._format_uptime(proc_uptime)
return "", f"Uptime {uptime_str}"
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return "", "Выключен"
except Exception as e:
logger.error(f"Ошибка при проверке локального процесса {process_name}: {e}")
return "", "Выключен"
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:
"""Расчет процента загрузки диска на основе реальной скорости I/O"""
try:
# Получаем текущую статистику диска
current_disk_io = self._get_disk_io_counters()
if current_disk_io is None:
return 0
current_time = time.time()
# Если это первое измерение, инициализируем
if self.last_disk_io_for_percent is None or self.last_disk_io_time_for_percent is None:
logger.debug("Первое измерение диска для процента, инициализируем базовые значения")
self.last_disk_io_for_percent = current_disk_io
self.last_disk_io_time_for_percent = current_time
return 0
# Рассчитываем время между измерениями
time_diff = current_time - self.last_disk_io_time_for_percent
if time_diff < 0.1: # Минимальный интервал 0.1 секунды для более точных измерений
logger.debug(f"Интервал между измерениями слишком мал: {time_diff:.3f}s, возвращаем 0%")
return 0
# Рассчитываем скорость операций в секунду
read_ops_diff = current_disk_io.read_count - self.last_disk_io_for_percent.read_count
write_ops_diff = current_disk_io.write_count - self.last_disk_io_for_percent.write_count
read_ops_per_sec = read_ops_diff / time_diff
write_ops_per_sec = write_ops_diff / time_diff
total_ops_per_sec = read_ops_per_sec + write_ops_per_sec
# Рассчитываем скорость передачи данных в байтах в секунду
read_bytes_diff = current_disk_io.read_bytes - self.last_disk_io_for_percent.read_bytes
write_bytes_diff = current_disk_io.write_bytes - self.last_disk_io_for_percent.write_bytes
read_bytes_per_sec = read_bytes_diff / time_diff
write_bytes_per_sec = write_bytes_diff / time_diff
total_bytes_per_sec = read_bytes_per_sec + write_bytes_per_sec
# Обновляем предыдущие значения для процента
self.last_disk_io_for_percent = current_disk_io
self.last_disk_io_time_for_percent = current_time
# Определяем максимальную производительность диска в зависимости от ОС
if self.os_type == "macos":
# macOS обычно имеет SSD с высокой производительностью
max_ops_per_sec = 50000 # Операций в секунду
max_bytes_per_sec = 3 * (1024**3) # 3 GB/s
elif self.os_type == "ubuntu":
# Ubuntu может быть на разных типах дисков
max_ops_per_sec = 30000 # Операций в секунду
max_bytes_per_sec = 2 * (1024**3) # 2 GB/s
else:
max_ops_per_sec = 40000
max_bytes_per_sec = 2.5 * (1024**3)
# Рассчитываем процент загрузки на основе операций и байтов
# Защита от деления на ноль
if max_ops_per_sec > 0:
ops_percent = min(100, (total_ops_per_sec / max_ops_per_sec) * 100)
else:
ops_percent = 0
if max_bytes_per_sec > 0:
bytes_percent = min(100, (total_bytes_per_sec / max_bytes_per_sec) * 100)
else:
bytes_percent = 0
# Взвешенный средний процент (операции важнее для большинства случаев)
final_percent = (ops_percent * 0.7) + (bytes_percent * 0.3)
# Логируем для отладки (только при высоких значениях)
if final_percent > 10:
logger.debug(f"Диск I/O: {total_ops_per_sec:.1f} ops/s, {total_bytes_per_sec/(1024**2):.1f} MB/s, "
f"Загрузка: {final_percent:.1f}% (ops: {ops_percent:.1f}%, bytes: {bytes_percent:.1f}%)")
# Округляем до целого числа
return round(final_percent)
except Exception as e:
logger.error(f"Ошибка при расчете процента загрузки диска: {e}")
return 0
def get_metrics_data(self) -> Dict:
"""Получение данных для метрик Prometheus"""
system_info = self.get_system_info()
if not system_info:
return {}
return {
'cpu_usage_percent': system_info.get('cpu_percent', 0),
'ram_usage_percent': system_info.get('ram_percent', 0),
'disk_usage_percent': system_info.get('disk_percent', 0),
'load_average_1m': system_info.get('load_avg_1m', 0),
'load_average_5m': system_info.get('load_avg_5m', 0),
'load_average_15m': system_info.get('load_avg_15m', 0),
'swap_usage_percent': system_info.get('swap_percent', 0),
'disk_io_percent': system_info.get('disk_io_percent', 0),
'system_uptime_seconds': self._get_system_uptime(),
'monitor_uptime_seconds': time.time() - self.monitor_start_time
}
def check_alerts(self, system_info: Dict) -> Tuple[bool, Optional[str]]:
"""Проверка необходимости отправки алертов с учетом задержек"""
current_time = time.time()
alerts = []
recoveries = []
# Проверка CPU с задержкой
if system_info['cpu_percent'] > self.threshold:
if not self.alert_states['cpu']:
# Первое превышение порога
if self.alert_start_times['cpu'] is None:
self.alert_start_times['cpu'] = current_time
logger.debug(f"CPU превысил порог {self.threshold}%: {system_info['cpu_percent']:.1f}% - начинаем отсчет задержки {self.alert_delays['cpu']}s")
# Проверяем, прошла ли задержка
if self.alert_delays['cpu'] == 0 or current_time - self.alert_start_times['cpu'] >= self.alert_delays['cpu']:
self.alert_states['cpu'] = True
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 ниже порога - сбрасываем состояние
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
# Проверка RAM с задержкой
if system_info['ram_percent'] > self.threshold:
if not self.alert_states['ram']:
# Первое превышение порога
if self.alert_start_times['ram'] is None:
self.alert_start_times['ram'] = current_time
logger.debug(f"RAM превысил порог {self.threshold}%: {system_info['ram_percent']:.1f}% - начинаем отсчет задержки {self.alert_delays['ram']}s")
# Проверяем, прошла ли задержка
if self.alert_delays['ram'] == 0 or current_time - self.alert_start_times['ram'] >= self.alert_delays['ram']:
self.alert_states['ram'] = True
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 ниже порога - сбрасываем состояние
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
# Проверка диска с задержкой
if system_info['disk_percent'] > self.threshold:
if not self.alert_states['disk']:
# Первое превышение порога
if self.alert_start_times['disk'] is None:
self.alert_start_times['disk'] = current_time
logger.debug(f"Disk превысил порог {self.threshold}%: {system_info['disk_percent']:.1f}% - начинаем отсчет задержки {self.alert_delays['disk']}s")
# Проверяем, прошла ли задержка
if self.alert_delays['disk'] == 0 or current_time - self.alert_start_times['disk'] >= self.alert_delays['disk']:
self.alert_states['disk'] = True
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
return alerts, recoveries
def _get_host_psutil(self):
"""Получение psutil с доступом к хосту"""
if self.is_docker_host_monitoring:
# Переключаемся на директории хоста
os.environ['PROC_ROOT'] = '/host/proc'
os.environ['SYS_ROOT'] = '/host/sys'
# Перезагружаем psutil для использования новых путей
import importlib
import psutil
importlib.reload(psutil)
return psutil
return psutil
def _get_host_cpu_info(self):
"""Получение информации о CPU хоста"""
try:
if self.is_docker_host_monitoring:
# Читаем информацию о CPU напрямую из /proc
with open('/host/proc/cpuinfo', 'r') as f:
cpu_info = f.read()
# Подсчитываем количество ядер
cpu_count = cpu_info.count('processor')
# Читаем load average
with open('/host/proc/loadavg', 'r') as f:
load_avg = f.read().strip().split()[:3]
load_avg = [float(x) for x in load_avg]
# Читаем статистику CPU
with open('/host/proc/stat', 'r') as f:
cpu_stat = f.readline().strip().split()[1:]
cpu_stat = [int(x) for x in cpu_stat]
# Рассчитываем процент CPU (упрощенный метод)
# В реальности нужно сравнивать с предыдущими значениями
cpu_percent = 0.0 # Будет рассчитано в get_system_info
return {
'cpu_count': cpu_count,
'load_avg': load_avg,
'cpu_stat': cpu_stat
}
else:
# Используем стандартный psutil
return {
'cpu_count': psutil.cpu_count(),
'load_avg': psutil.getloadavg(),
'cpu_stat': None
}
except Exception as e:
logger.error(f"Ошибка при получении информации о CPU хоста: {e}")
return None
def _get_host_memory_info(self):
"""Получение информации о памяти хоста"""
try:
if self.is_docker_host_monitoring:
# Читаем информацию о памяти из /proc/meminfo
with open('/host/proc/meminfo', 'r') as f:
mem_info = f.read()
# Парсим значения
mem_lines = mem_info.split('\n')
mem_data = {}
for line in mem_lines:
if ':' in line:
key, value = line.split(':', 1)
mem_data[key.strip()] = int(value.strip().split()[0]) * 1024 # Конвертируем в байты
# Рассчитываем проценты
total = mem_data.get('MemTotal', 0)
available = mem_data.get('MemAvailable', 0)
used = total - available
ram_percent = (used / total * 100) if total > 0 else 0
# Swap
swap_total = mem_data.get('SwapTotal', 0)
swap_free = mem_data.get('SwapFree', 0)
swap_used = swap_total - swap_free
swap_percent = (swap_used / swap_total * 100) if swap_total > 0 else 0
return {
'ram_total': total,
'ram_used': used,
'ram_percent': ram_percent,
'swap_total': swap_total,
'swap_used': swap_used,
'swap_percent': swap_percent
}
else:
# Используем стандартный psutil
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
return {
'ram_total': memory.total,
'ram_used': memory.used,
'ram_percent': memory.percent,
'swap_total': swap.total,
'swap_used': swap.used,
'swap_percent': swap.percent
}
except Exception as e:
logger.error(f"Ошибка при получении информации о памяти хоста: {e}")
return None
def _get_host_disk_info(self):
"""Получение информации о диске хоста"""
try:
if self.is_docker_host_monitoring:
# Используем df для получения информации о диске
import subprocess
result = subprocess.run(['df', '/'], capture_output=True, text=True)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')
if len(lines) >= 2:
parts = lines[1].split()
if len(parts) >= 4:
total_kb = int(parts[1])
used_kb = int(parts[2])
available_kb = int(parts[3])
total = total_kb * 1024
used = used_kb * 1024
available = available_kb * 1024
percent = (used / total * 100) if total > 0 else 0
return {
'total': total,
'used': used,
'free': available,
'percent': percent
}
# Fallback к стандартному psutil
return None
else:
# Используем стандартный psutil
return None
except Exception as e:
logger.error(f"Ошибка при получении информации о диске хоста: {e}")
return None
def _check_helper_bot_status(self) -> Tuple[str, str]:
"""Проверка статуса helper_bot через HTTP endpoint"""
try:
import requests
logger.info("Проверяем статус helper_bot через HTTP endpoint /status")
# Обращаемся к endpoint /status в helper_bot
url = 'http://bots_telegram_bot:8080/status'
logger.info(f"Отправляем HTTP запрос к: {url}")
response = requests.get(url, timeout=5)
logger.info(f"Получен HTTP ответ: статус {response.status_code}")
if response.status_code == 200:
try:
data = response.json()
logger.info(f"Получены данные: {data}")
status = data.get('status', 'unknown')
uptime = data.get('uptime', 'unknown')
if status == 'running':
result = "", f"Uptime {uptime}"
logger.info(f"Helper_bot работает: {result}")
return result
elif status == 'starting':
result = "🔄", f"Запуск: {uptime}"
logger.info(f"Helper_bot запускается: {result}")
return result
else:
result = "⚠️", f"Статус: {status}"
logger.warning(f"Helper_bot необычный статус: {result}")
return result
except (ValueError, KeyError) as e:
# Если не удалось распарсить JSON, но статус 200
logger.warning(f"Не удалось распарсить JSON ответ: {e}, но статус 200")
result = "", "HTTP: доступен"
logger.info(f"Helper_bot доступен: {result}")
return result
else:
logger.warning(f"HTTP статус не 200: {response.status_code}")
return "⚠️", f"HTTP: {response.status_code}"
except requests.exceptions.Timeout:
logger.error("HTTP запрос к helper_bot завершился таймаутом")
return "⚠️", "HTTP: таймаут"
except requests.exceptions.ConnectionError as e:
logger.error(f"HTTP ошибка соединения с helper_bot: {e}")
return "", "HTTP: нет соединения"
except ImportError:
logger.debug("requests не доступен для HTTP проверки")
return "", "HTTP: requests недоступен"
except Exception as e:
logger.error(f"Неожиданная ошибка при HTTP проверке helper_bot: {e}")
return "", f"HTTP: ошибка"