From f1fc514a3356d2819de21daa3b18f9d4dfb596b8 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 17 Apr 2026 10:11:16 +0800 Subject: [PATCH] fix: Agent network and disk metrics for VPS Network: - Remove speed > 0 check in _is_real_interface() - Use 1000 Mbps default for interfaces with speed=0 - This allows virtual interfaces (ens3) to be detected - Remove network_rx/network_tx (no longer needed) Disk: - Rewrite get_disk_metrics() to detect unique devices - If only one device (/) - create only disk_used_root - If multiple devices - create for each - This fixes VPS with single disk showing same % for all mounts --- __pycache__/agent.cpython-312.pyc | Bin 16608 -> 15783 bytes agent.py | 105 ++++++++++++++---------------- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/__pycache__/agent.cpython-312.pyc b/__pycache__/agent.cpython-312.pyc index 192018ac1670b898afb88c0374c9c0c73a55eca5..f17a4caab74c9aee326ab7d66c7d2dc2be521433 100644 GIT binary patch delta 4000 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 0 else 1000 speed_bps = speed_mbps * 1000000 / 8 if name in _prev_net_io: prev = _prev_net_io[name] @@ -78,38 +76,63 @@ def _is_real_partition(mountpoint, fstype): def get_disk_metrics(): - """Собираем метрики диска для примонтированных разделов""" + """Собираем метрики диска для реальных разделов""" metrics = {} - total_used = 0 - total_capacity = 0 - - priority_mounts = ['/', '/home', '/boot', '/var', '/opt', '/data', '/mnt', '/srv', '/tmp'] - - for mountpoint in priority_mounts: - try: - usage = psutil.disk_usage(mountpoint) - name = mountpoint.strip('/').replace('/', '_') or 'root' - if name not in metrics: - metrics[f'disk_used_{name}'] = round(usage.percent, 1) - total_used += usage.used - total_capacity += usage.total - except (PermissionError, OSError, FileNotFoundError): - pass + skip_fstypes = {'tmpfs', 'devtmpfs', 'overlay', 'squashfs', 'snap', + 'devpts', 'proc', 'sysfs', 'cgroup', 'cgroup2', + 'pstore', 'hugetlbfs', 'mqueue', 'debugfs', + 'tracefs', 'bpf', 'fusectl', 'configfs', + 'securityfs', 'ramfs'} + skip_mounts = {'/run', '/run/lock', '/sys', '/proc', '/dev', + '/dev/shm', '/dev/pts', '/sys/fs/cgroup'} + # Собираем реальные разделы с их устройствами + partitions = [] for part in psutil.disk_partitions(all=False): - name = part.mountpoint.strip('/').replace('/', '_') or 'root' - if name in metrics: + if part.fstype in skip_fstypes: continue - if not _is_real_partition(part.mountpoint, part.fstype): + if part.mountpoint in skip_mounts: + continue + if part.mountpoint == '/boot/efi': continue try: usage = psutil.disk_usage(part.mountpoint) - metrics[f'disk_used_{name}'] = round(usage.percent, 1) + partitions.append({ + 'mountpoint': part.mountpoint, + 'device': part.device, + 'usage': usage + }) except (PermissionError, OSError): pass - if total_capacity > 0: - metrics['disk_used'] = round((total_used / total_capacity) * 100, 1) + # Определяем уникальные устройства + devices = {} + for p in partitions: + dev = p['device'] + if dev not in devices: + devices[dev] = [] + devices[dev].append(p) + + # Если одно устройство - только / + # Если несколько - для каждого отдельного устройства + if len(devices) == 1: + # Один диск - только корень + for p in partitions: + if p['mountpoint'] == '/': + metrics['disk_used_root'] = round(p['usage'].percent, 1) + metrics['disk_total_gb_root'] = round(p['usage'].total / (1024**3), 1) + metrics['disk_used'] = round(p['usage'].percent, 1) + break + else: + # Несколько устройств - собираем для каждого + for dev, parts in devices.items(): + for p in parts: + mp = p['mountpoint'] + name = mp.strip('/').replace('/', '_') or 'root' + metrics[f'disk_used_{name}'] = round(p['usage'].percent, 1) + metrics[f'disk_total_gb_{name}'] = round(p['usage'].total / (1024**3), 1) + if mp == '/': + metrics['disk_used'] = round(p['usage'].percent, 1) return metrics @@ -122,12 +145,6 @@ def get_metrics(): # Дисковые метрики для всех реальных разделов disk_metrics = get_disk_metrics() - # Получаем сетевую статистику - try: - net_io = psutil.net_io_counters() - except: - net_io = None - result = { 'cpu_load': cpu_percent, 'ram_used': memory.percent, @@ -137,37 +154,13 @@ def get_metrics(): # Метрики использования сети net_metrics = get_network_metrics() result.update(net_metrics) + # RAM total GB result["ram_total_gb"] = round(memory.total / (1024**3), 1) - # Disk total GB - сначала приоритетные mountpoints - priority_mounts = ['/', '/home', '/boot', '/var', '/opt', '/data', '/mnt', '/srv', '/tmp'] - for mountpoint in priority_mounts: - try: - usage = psutil.disk_usage(mountpoint) - name = mountpoint.strip("/").replace("/", "_") or "root" - if f"disk_total_gb_{name}" not in result: - result[f"disk_total_gb_{name}"] = round(usage.total / (1024**3), 1) - except (PermissionError, OSError, FileNotFoundError): - pass - - for part in psutil.disk_partitions(all=False): - try: - usage = psutil.disk_usage(part.mountpoint) - name = part.mountpoint.strip("/").replace("/", "_") or "root" - if f"disk_total_gb_{name}" not in result: - result[f"disk_total_gb_{name}"] = round(usage.total / (1024**3), 1) - except (PermissionError, OSError): - pass - if net_metrics: print(f" Сетевые метрики: {net_metrics}") - # Сетевые метрики - if net_io: - result['network_rx'] = round(net_io.bytes_recv / (1024 * 1024), 2) - result['network_tx'] = round(net_io.bytes_sent / (1024 * 1024), 2) - return result def get_top_processes(process_type='cpu'):