From 7d970eada775d830cd4eff7bf757a4343a9f8150 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Mon, 15 Sep 2025 14:13:22 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20proxvmsystem=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен новый коллектор proxvmsystem, объединяющий функциональность system и gpu коллекторов - Добавлен machine_uid для идентификации VM/контейнеров в Proxmox инфраструктуре - Обновлена документация по коллекторам и проекту - Добавлены новые хосты в inventory для тестирования - Обновлен Makefile для сборки нового коллектора - Обновлен config.yaml с конфигурацией proxvmsystem коллектора Автор: Сергей Антропов Сайт: https://devops.org.ru --- Makefile | 3 +- README.md | 2 + bin/agent/config.yaml | 8 + docs/collectors.md | 2 + docs/collectors/proxvmsystem.md | 302 ++++++++ runner/inventory.ini | 7 +- src/collectors/proxvmsystem/main.go | 55 ++ .../proxvmsystem/proxvmsystem_linux.go | 646 ++++++++++++++++++ .../proxvmsystem/proxvmsystem_unsupported.go | 18 + 9 files changed, 1040 insertions(+), 3 deletions(-) create mode 100644 docs/collectors/proxvmsystem.md create mode 100644 src/collectors/proxvmsystem/main.go create mode 100644 src/collectors/proxvmsystem/proxvmsystem_linux.go create mode 100644 src/collectors/proxvmsystem/proxvmsystem_unsupported.go diff --git a/Makefile b/Makefile index 1258049..f18787e 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,8 @@ collectors-linux: CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \ CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \ CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode && \ - CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvms ./src/collectors/proxvms" + CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvms ./src/collectors/proxvms && \ + CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvmsystem ./src/collectors/proxvmsystem" collectors-windows: # Кросс-сборка коллекторов для Windows diff --git a/README.md b/README.md index bc3035d..72dfd71 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ SensusAgent — модульный агент сбора метрик. Аген - Деплой (Ansible, systemd): `docs/deploy.md` - **Kafka SSL поддержка**: `docs/kafka_ssl.md` ⭐ - **Proxmox кластер**: `docs/collectors/proxcluster.md` ⭐ +- **Proxmox VM/контейнеры**: `docs/collectors/proxvms.md` ⭐ +- **Proxmox системные метрики**: `docs/collectors/proxvmsystem.md` ⭐ Быстрый старт: ```bash diff --git a/bin/agent/config.yaml b/bin/agent/config.yaml index 8e21438..ca7f034 100644 --- a/bin/agent/config.yaml +++ b/bin/agent/config.yaml @@ -118,5 +118,13 @@ collectors: timeout: "300s" exec: "./collectors/proxvms" platforms: [linux] + proxvmsystem: + enabled: true + type: exec + key: proxvmsystem + interval: "300s" + timeout: "60s" + exec: "./collectors/proxvmsystem" + platforms: [linux] diff --git a/docs/collectors.md b/docs/collectors.md index 5601770..2e2ed74 100644 --- a/docs/collectors.md +++ b/docs/collectors.md @@ -61,6 +61,7 @@ make collectors-darwin # darwin/arm64 - **proxcluster** - информация о Proxmox кластере (ноды, хранилища, кворум) - Linux ⭐ - **proxnode** - информация о Proxmox ноде (ресурсы, сервисы, диски) - Linux ⭐ - **proxvms** - информация о виртуальных машинах и контейнерах Proxmox - Linux ⭐ +- **proxvmsystem** - системные метрики с machine_uid для Proxmox VM/контейнеров - Linux ⭐ ### Документация коллекторов @@ -68,6 +69,7 @@ make collectors-darwin # darwin/arm64 - [gpu](collectors/gpu.md) - сбор информации о GPU устройствах с агрегированной статистикой - [proxnode](collectors/proxnode.md) - сбор информации о Proxmox ноде - [proxvms](collectors/proxvms.md) - сбор информации о виртуальных машинах и контейнерах Proxmox +- [proxvmsystem](collectors/proxvmsystem.md) - системные метрики с machine_uid для Proxmox VM/контейнеров - [system](collectors/system.md) - сбор системных метрик - [docker](collectors/docker.md) - сбор информации о Docker контейнерах - [hba](collectors/hba.md) - сбор информации о RAID/HBA контроллерах diff --git a/docs/collectors/proxvmsystem.md b/docs/collectors/proxvmsystem.md new file mode 100644 index 0000000..d9dbc51 --- /dev/null +++ b/docs/collectors/proxvmsystem.md @@ -0,0 +1,302 @@ +# Коллектор proxvmsystem + +**Автор:** Сергей Антропов +**Сайт:** https://devops.org.ru +**Платформа:** Linux +**Тип:** exec (Go-бинарник) + +## Описание + +Коллектор `proxvmsystem` объединяет функциональность коллекторов `system` и `gpu`, добавляя уникальный идентификатор `machine_uid` для идентификации виртуальной машины или контейнера в Proxmox инфраструктуре. + +## Назначение + +- Сбор системных метрик (CPU, RAM, сеть, диски, время синхронизации, обновления) +- Сбор информации о GPU устройствах (NVIDIA/AMD) +- Генерация уникального `machine_uid` на основе `/etc/machine-id` или `/var/lib/dbus/machine-id` +- Предоставление единого интерфейса для мониторинга VM/контейнеров с привязкой к уникальному идентификатору + +## Конфигурация + +```yaml +proxvmsystem: + enabled: true + type: exec + key: proxvmsystem + interval: "300s" + timeout: "60s" + exec: "./collectors/proxvmsystem" + platforms: [linux] +``` + +## Выходные данные + +### Структура JSON + +```json +{ + "collector_name": "proxvmsystem", + "machine_uid": "e7b3e8ed05a8d0c9", + "cpu": { + "usage_percent": 15.2, + "cores": 4, + "load_avg": [0.8, 1.2, 1.5] + }, + "ram": { + "total_gb": 16.0, + "used_gb": 8.5, + "free_gb": 7.5, + "usage_percent": 53.1 + }, + "swap": { + "total_gb": 2.0, + "used_gb": 0.1, + "free_gb": 1.9, + "usage_percent": 5.0 + }, + "network": { + "interfaces": [ + { + "name": "eth0", + "speed_mbps": 1000, + "mac": "00:11:22:33:44:55", + "ips": ["10.14.246.75/24"], + "rx_bytes": 1024000, + "tx_bytes": 2048000 + } + ] + }, + "disks": [ + { + "device": "/dev/sda", + "mountpoint": "/", + "total_gb": 50.0, + "used_gb": 25.0, + "free_gb": 25.0, + "usage_percent": 50.0 + } + ], + "time_sync": { + "ntp_sync": true, + "ntp_offset_ms": 0.5, + "system_time": "2024-01-15T10:30:00Z" + }, + "updates": { + "available": 5, + "security": 2, + "last_check": "2024-01-15T09:00:00Z" + }, + "gpu": [ + { + "id": "0", + "name": "NVIDIA GeForce RTX 3080", + "driver_version": "470.86", + "memory_total_mb": 10240, + "memory_used_mb": 2048, + "memory_free_mb": 8192, + "utilization_percent": 25.0, + "temperature_c": 65, + "power_usage_w": 180 + } + ], + "gpu_summary": { + "total_gpus": 1, + "total_memory_mb": 10240, + "total_utilization_percent": 25.0, + "total_power_usage_w": 180 + }, + "execution_time_ms": 1250, + "execution_time_seconds": 1.25 +} +``` + +### Поля данных + +#### Основные поля + +- **`collector_name`** (string) - имя коллектора: "proxvmsystem" +- **`machine_uid`** (string) - уникальный идентификатор машины (SHA256 хэш от machine-id) + +#### Системные метрики + +- **`cpu`** (object) - информация о процессоре + - `usage_percent` (float) - загрузка CPU в процентах + - `cores` (int) - количество ядер + - `load_avg` (array) - средняя загрузка за 1, 5, 15 минут + +- **`ram`** (object) - информация об оперативной памяти + - `total_gb` (float) - общий объем RAM в GB + - `used_gb` (float) - используемый объем RAM в GB + - `free_gb` (float) - свободный объем RAM в GB + - `usage_percent` (float) - процент использования RAM + +- **`swap`** (object) - информация о swap + - `total_gb` (float) - общий объем swap в GB + - `used_gb` (float) - используемый объем swap в GB + - `free_gb` (float) - свободный объем swap в GB + - `usage_percent` (float) - процент использования swap + +- **`network`** (object) - информация о сетевых интерфейсах + - `interfaces` (array) - массив сетевых интерфейсов + - `name` (string) - имя интерфейса + - `speed_mbps` (int) - скорость в Mbps + - `mac` (string) - MAC адрес + - `ips` (array) - IP адреса + - `rx_bytes` (int) - получено байт + - `tx_bytes` (int) - отправлено байт + +- **`disks`** (array) - информация о дисках + - `device` (string) - устройство + - `mountpoint` (string) - точка монтирования + - `total_gb` (float) - общий размер в GB + - `used_gb` (float) - используемый размер в GB + - `free_gb` (float) - свободный размер в GB + - `usage_percent` (float) - процент использования + +- **`time_sync`** (object) - информация о синхронизации времени + - `ntp_sync` (bool) - синхронизация с NTP + - `ntp_offset_ms` (float) - смещение от NTP в миллисекундах + - `system_time` (string) - системное время + +- **`updates`** (object) - информация об обновлениях + - `available` (int) - количество доступных обновлений + - `security` (int) - количество обновлений безопасности + - `last_check` (string) - время последней проверки + +#### GPU метрики + +- **`gpu`** (array) - информация о GPU устройствах + - `id` (string) - ID GPU + - `name` (string) - название GPU + - `driver_version` (string) - версия драйвера + - `memory_total_mb` (int) - общая память в MB + - `memory_used_mb` (int) - используемая память в MB + - `memory_free_mb` (int) - свободная память в MB + - `utilization_percent` (float) - загрузка GPU в процентах + - `temperature_c` (int) - температура в градусах Цельсия + - `power_usage_w` (int) - потребление энергии в ваттах + +- **`gpu_summary`** (object) - агрегированная информация о GPU + - `total_gpus` (int) - общее количество GPU + - `total_memory_mb` (int) - общая память всех GPU в MB + - `total_utilization_percent` (float) - средняя загрузка GPU + - `total_power_usage_w` (int) - общее потребление энергии + +#### Метаданные + +- **`execution_time_ms`** (int) - время выполнения коллектора в миллисекундах +- **`execution_time_seconds`** (float) - время выполнения коллектора в секундах + +## Генерация machine_uid + +Коллектор генерирует уникальный идентификатор `machine_uid` на основе machine-id системы: + +1. **Чтение machine-id**: Пытается прочитать из `/etc/machine-id` или `/var/lib/dbus/machine-id` +2. **Генерация хэша**: Создает SHA256 хэш от machine-id +3. **Усечение**: Берет первые 16 символов хэша для компактности + +```go +func getMachineIDFromHost() string { + paths := []string{ + "/etc/machine-id", + "/var/lib/dbus/machine-id", + } + + for _, path := range paths { + if data, err := os.ReadFile(path); err == nil { + machineID := strings.TrimSpace(string(data)) + if machineID != "" { + hash := sha256.Sum256([]byte(machineID)) + return hex.EncodeToString(hash[:])[:16] + } + } + } + + return "" +} +``` + +## Зависимости + +### Системные команды + +- **`df`** - информация о дисках +- **`ntpq`** - информация о NTP синхронизации +- **`timedatectl`** - информация о времени системы +- **`apt`** - информация об обновлениях (Ubuntu/Debian) +- **`yum`** - информация об обновлениях (CentOS/RHEL) + +### GPU команды + +- **`nvidia-smi`** - информация о NVIDIA GPU +- **`rocm-smi`** - информация об AMD GPU + +### Файлы системы + +- **`/proc/stat`** - статистика CPU +- **`/proc/meminfo`** - информация о памяти +- **`/proc/loadavg`** - средняя загрузка системы +- **`/sys/class/net//speed`** - скорость сетевых интерфейсов +- **`/etc/machine-id`** - уникальный идентификатор машины +- **`/var/lib/dbus/machine-id`** - альтернативный путь к machine-id + +## Использование + +### Запуск коллектора + +```bash +# Прямой запуск +./collectors/proxvmsystem + +# С переменными окружения +COLLECTOR_TIMEOUT=60s ./collectors/proxvmsystem +``` + +### Интеграция с агентом + +Коллектор автоматически запускается агентом согласно конфигурации в `config.yaml`. + +## Ограничения + +- Работает только на Linux системах +- Требует права на чтение системных файлов +- GPU метрики доступны только при наличии соответствующих драйверов +- Время выполнения может варьироваться в зависимости от количества GPU и системных ресурсов + +## Логирование + +Коллектор выводит отладочную информацию в stderr для диагностики проблем: + +- Ошибки чтения системных файлов +- Проблемы с выполнением команд +- Ошибки парсинга данных + +## Примеры использования + +### Мониторинг VM в Proxmox + +```bash +# Получение machine_uid VM +ssh vm-host "./collectors/proxvmsystem" | jq '.machine_uid' + +# Сравнение с данными от proxvms коллектора на Proxmox ноде +ssh proxmox-node "./collectors/proxvms" | jq '.vms[] | select(.machine_uid == "e7b3e8ed05a8d0c9")' +``` + +### Мониторинг производительности + +```bash +# Загрузка CPU и GPU +./collectors/proxvmsystem | jq '{cpu: .cpu.usage_percent, gpu: .gpu_summary.total_utilization_percent}' + +# Использование памяти +./collectors/proxvmsystem | jq '{ram: .ram.usage_percent, swap: .swap.usage_percent}' +``` + +## Связанные коллекторы + +- **`system`** - базовые системные метрики +- **`gpu`** - информация о GPU устройствах +- **`proxvms`** - информация о VM/контейнерах Proxmox +- **`proxnode`** - информация о Proxmox нодах +- **`proxcluster`** - информация о Proxmox кластере diff --git a/runner/inventory.ini b/runner/inventory.ini index b0e303b..01b2070 100644 --- a/runner/inventory.ini +++ b/runner/inventory.ini @@ -4,5 +4,8 @@ #kube_ansible ansible_host=10.14.246.9 ansible_user=devops #videotest7 ansible_host=10.13.37.186 ansible_user=devops #videotest8 ansible_host=10.13.37.187 ansible_user=devops -pnode02 ansible_host=10.14.253.12 ansible_user=devops -#dbrain01 ansible_host=10.14.246.75 ansible_user=devops +#pnode02 ansible_host=10.14.253.12 ansible_user=devops +#pnode06 ansible_host=10.14.253.16 ansible_user=devops +pnode10 ansible_host=10.14.253.20 ansible_user=devops +kube-dbrain-node01 ansible_host=10.14.246.75 ansible_user=devops +kube-data ansible_host=10.14.246.150 ansible_user=devops diff --git a/src/collectors/proxvmsystem/main.go b/src/collectors/proxvmsystem/main.go new file mode 100644 index 0000000..db4ac14 --- /dev/null +++ b/src/collectors/proxvmsystem/main.go @@ -0,0 +1,55 @@ +package main + +// Автор: Сергей Антропов, сайт: https://devops.org.ru +// Коллектор proxvmsystem - объединяет функциональность system и gpu коллекторов +// с добавлением machine_uid для идентификации VM/контейнера + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" +) + +// collectProxVMSystem реализуется в файлах с билд-тегами под конкретные ОС. + +func main() { + // Засекаем время начала выполнения + startTime := time.Now() + + // Таймаут можно переопределить окружением COLLECTOR_TIMEOUT + timeout := parseDurationOr("COLLECTOR_TIMEOUT", 15*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + data, err := collectProxVMSystem(ctx) + if err != nil || data == nil { + fmt.Println("{}") + return + } + + // Вычисляем время выполнения + executionTime := time.Since(startTime) + + // Добавляем время выполнения в результат + data["execution_time_ms"] = executionTime.Milliseconds() + data["execution_time_seconds"] = executionTime.Seconds() + + enc := json.NewEncoder(os.Stdout) + enc.SetEscapeHTML(false) + _ = enc.Encode(data) +} + +func parseDurationOr(env string, def time.Duration) time.Duration { + v := strings.TrimSpace(os.Getenv(env)) + if v == "" { + return def + } + d, err := time.ParseDuration(v) + if err != nil { + return def + } + return d +} diff --git a/src/collectors/proxvmsystem/proxvmsystem_linux.go b/src/collectors/proxvmsystem/proxvmsystem_linux.go new file mode 100644 index 0000000..8ba9c18 --- /dev/null +++ b/src/collectors/proxvmsystem/proxvmsystem_linux.go @@ -0,0 +1,646 @@ +//go:build linux + +package main + +// Автор: Сергей Антропов, сайт: https://devops.org.ru +// Коллектор proxvmsystem для Linux - объединяет system и gpu с machine_uid + +import ( + "bufio" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +// collectProxVMSystem собирает системные данные (CPU, RAM, GPU, сеть, диски) +// с добавлением machine_uid для идентификации VM/контейнера +func collectProxVMSystem(ctx context.Context) (map[string]any, error) { + result := map[string]any{ + "collector_name": "proxvmsystem", + } + + // Получаем machine_uid + machineUID := getMachineUID() + if machineUID != "" { + result["machine_uid"] = machineUID + } + + // Собираем системные данные (из system коллектора) + cpu, err := collectCPU(ctx) + if err == nil { + result["cpu"] = cpu + } + + ram, swap, err := collectMem(ctx) + if err == nil { + result["ram"] = ram + result["swap"] = swap + } + + netIfs, err := collectNet(ctx) + if err == nil { + result["network"] = netIfs + } + + disks, err := collectDisks(ctx) + if err == nil { + result["disks"] = disks + } + + ts, err := collectTimeSync(ctx) + if err == nil { + result["time_sync"] = ts + } + + updates, err := collectUpdates(ctx) + if err == nil { + result["updates"] = updates + } + + // Собираем GPU данные (из gpu коллектора) + gpuData, err := collectGPUData(ctx) + if err == nil && gpuData != nil { + if gpuArray, ok := gpuData["gpu"].([]map[string]any); ok && len(gpuArray) > 0 { + result["gpu"] = gpuArray + } + if summary, ok := gpuData["summary"].(map[string]any); ok { + result["gpu_summary"] = summary + } + } + + // Если нет данных, возвращаем ошибку + if len(result) <= 2 { // только collector_name и возможно machine_uid + return nil, errors.New("no data collected") + } + + return result, nil +} + +// getMachineUID получает machine_uid для текущей системы +func getMachineUID() string { + // Пытаемся получить UUID из DMI (для QEMU VM) + if uuid := getDMISystemUUID(); uuid != "" { + hash := sha256.Sum256([]byte(uuid)) + return hex.EncodeToString(hash[:])[:16] + } + + // Пытаемся получить machine-id (для LXC контейнеров) + if machineID := getSystemMachineID(); machineID != "" { + hash := sha256.Sum256([]byte(machineID)) + return hex.EncodeToString(hash[:])[:16] + } + + return "" +} + +// getDMISystemUUID получает UUID из DMI (для QEMU VM) +func getDMISystemUUID() string { + // Пробуем /sys/class/dmi/id/product_uuid + if data, err := os.ReadFile("/sys/class/dmi/id/product_uuid"); err == nil { + uuid := strings.TrimSpace(string(data)) + if uuid != "" && uuid != "00000000-0000-0000-0000-000000000000" { + return uuid + } + } + + // Fallback на dmidecode + if exists("dmidecode") { + cmd := exec.Command("dmidecode", "-s", "system-uuid") + if output, err := cmd.Output(); err == nil { + uuid := strings.TrimSpace(string(output)) + if uuid != "" && uuid != "00000000-0000-0000-0000-000000000000" { + return uuid + } + } + } + + return "" +} + +// getSystemMachineID получает machine-id системы (для LXC контейнеров) +func getSystemMachineID() string { + // Пробуем /etc/machine-id + if data, err := os.ReadFile("/etc/machine-id"); err == nil { + machineID := strings.TrimSpace(string(data)) + if machineID != "" { + return machineID + } + } + + // Fallback на /var/lib/dbus/machine-id + if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil { + machineID := strings.TrimSpace(string(data)) + if machineID != "" { + return machineID + } + } + + return "" +} + +// collectGPUData собирает данные о GPU (адаптировано из gpu коллектора) +func collectGPUData(ctx context.Context) (map[string]any, error) { + var gpuArray []map[string]any + + // Сначала пробуем NVIDIA + if exists("nvidia-smi") { + if arr := collectNvidia(ctx); len(arr) > 0 { + gpuArray = arr + } + } + // Затем AMD ROCm + if exists("rocm-smi") && len(gpuArray) == 0 { + if arr := collectRocm(ctx); len(arr) > 0 { + gpuArray = arr + } + } + + result := map[string]any{ + "gpu": gpuArray, + } + + // Добавляем агрегированную статистику + if len(gpuArray) > 0 { + result["summary"] = calculateGPUSummary(gpuArray) + } + + return result, nil +} + +// collectNvidia собирает данные о NVIDIA GPU +func collectNvidia(ctx context.Context) []map[string]any { + cmd := exec.CommandContext(ctx, "nvidia-smi", "--query-gpu=index,name,driver_version,memory.total,memory.used,memory.free,temperature.gpu,utilization.gpu,utilization.memory,power.draw,power.limit", "--format=csv,noheader,nounits") + output, err := cmd.Output() + if err != nil { + return nil + } + + var gpus []map[string]any + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + + for _, line := range lines { + if line == "" { + continue + } + + fields := strings.Split(line, ", ") + if len(fields) < 11 { + continue + } + + gpu := map[string]any{ + "index": parseFloat(fields[0]), + "name": strings.TrimSpace(fields[1]), + "driver_version": strings.TrimSpace(fields[2]), + "memory_total_mb": parseFloat(fields[3]), + "memory_used_mb": parseFloat(fields[4]), + "memory_free_mb": parseFloat(fields[5]), + "temperature_c": parseFloat(fields[6]), + "utilization_gpu": parseFloat(fields[7]), + "utilization_mem": parseFloat(fields[8]), + "power_draw_w": parseFloat(fields[9]), + "power_limit_w": parseFloat(fields[10]), + } + + gpus = append(gpus, gpu) + } + + return gpus +} + +// collectRocm собирает данные о AMD GPU +func collectRocm(ctx context.Context) []map[string]any { + cmd := exec.CommandContext(ctx, "rocm-smi", "--showid", "--showtemp", "--showuse", "--showmemuse", "--showmeminfo", "vram", "--showpower") + output, err := cmd.Output() + if err != nil { + return nil + } + + var gpus []map[string]any + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + + for _, line := range lines { + if line == "" || !strings.Contains(line, "card") { + continue + } + + // Парсим строку вида "card 0: AMD Radeon RX 580" + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + + cardPart := strings.TrimSpace(parts[0]) + namePart := strings.TrimSpace(parts[1]) + + // Извлекаем индекс карты + cardIndex := strings.TrimPrefix(cardPart, "card ") + + gpu := map[string]any{ + "index": parseFloat(cardIndex), + "name": namePart, + "driver_version": "ROCm", + "memory_total_mb": 0.0, // ROCm не предоставляет эту информацию + "memory_used_mb": 0.0, + "memory_free_mb": 0.0, + "temperature_c": 0.0, + "utilization_gpu": 0.0, + "utilization_mem": 0.0, + "power_draw_w": 0.0, + "power_limit_w": 0.0, + } + + gpus = append(gpus, gpu) + } + + return gpus +} + +// calculateGPUSummary вычисляет агрегированную статистику по GPU +func calculateGPUSummary(gpus []map[string]any) map[string]any { + if len(gpus) == 0 { + return map[string]any{} + } + + totalMemory := 0.0 + usedMemory := 0.0 + totalPower := 0.0 + avgTemp := 0.0 + avgUtil := 0.0 + + for _, gpu := range gpus { + if mem, ok := gpu["memory_total_mb"].(float64); ok { + totalMemory += mem + } + if mem, ok := gpu["memory_used_mb"].(float64); ok { + usedMemory += mem + } + if power, ok := gpu["power_draw_w"].(float64); ok { + totalPower += power + } + if temp, ok := gpu["temperature_c"].(float64); ok { + avgTemp += temp + } + if util, ok := gpu["utilization_gpu"].(float64); ok { + avgUtil += util + } + } + + count := float64(len(gpus)) + + return map[string]any{ + "gpu_count": len(gpus), + "total_memory_mb": totalMemory, + "used_memory_mb": usedMemory, + "total_power_w": totalPower, + "avg_temperature": avgTemp / count, + "avg_utilization": avgUtil / count, + } +} + +// parseFloat безопасно парсит строку в float64 +func parseFloat(s string) float64 { + if s == "" || s == "N/A" { + return 0.0 + } + f, _ := strconv.ParseFloat(strings.TrimSpace(s), 64) + return f +} + +// exists проверяет существование команды в PATH +func exists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +// --- CPU функции из system коллектора --- + +// cpuTimes — срез счётчиков CPU из /proc/stat для вычисления загрузки CPU. +type cpuTimes struct{ user, nice, system, idle, iowait, irq, softirq, steal, guest, guestNice uint64 } + +// readProcStat читает строку "cpu " из /proc/stat и возвращает агрегированные +// счётчики времени CPU для последующих дифф-вычислений загрузки. +func readProcStat() (cpuTimes, error) { + f, err := os.Open("/proc/stat") + if err != nil { return cpuTimes{}, err } + defer f.Close() + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + if !strings.HasPrefix(line, "cpu ") { continue } + fields := strings.Fields(line) + if len(fields) < 5 { break } + parse := func(i int) uint64 { if i < len(fields) { v,_ := strconv.ParseUint(fields[i],10,64); return v }; return 0 } + return cpuTimes{ + user: parse(1), nice: parse(2), system: parse(3), idle: parse(4), + iowait: parse(5), irq: parse(6), softirq: parse(7), steal: parse(8), guest: parse(9), guestNice: parse(10), + }, nil + } + return cpuTimes{}, errors.New("no cpu line") +} + +// cpuUsagePercent возвращает среднюю загрузку CPU в процентах за окно sampleMs. +func cpuUsagePercent(sampleMs int) (float64, error) { + a, err := readProcStat() + if err != nil { return 0, err } + time.Sleep(time.Duration(sampleMs) * time.Millisecond) + b, err := readProcStat() + if err != nil { return 0, err } + totalA := a.user+a.nice+a.system+a.idle+a.iowait+a.irq+a.softirq+a.steal + totalB := b.user+b.nice+b.system+b.idle+b.iowait+b.irq+b.softirq+b.steal + delta := float64(totalB-totalA) + busy := float64((b.user-a.user)+(b.nice-a.nice)+(b.system-a.system)+(b.irq-a.irq)+(b.softirq-a.softirq)+(b.steal-a.steal)) + if delta <= 0 { return 0, nil } + return (busy/delta)*100.0, nil +} + +// loadAvg читает 1/5/15-минутную среднюю загрузку системы из /proc/loadavg. +func loadAvg() (float64,float64,float64,error) { + b, err := os.ReadFile("/proc/loadavg") + if err != nil { return 0,0,0,err } + fields := strings.Fields(string(b)) + if len(fields) < 3 { return 0,0,0,errors.New("bad loadavg") } + l1,_ := strconv.ParseFloat(fields[0],64) + l5,_ := strconv.ParseFloat(fields[1],64) + l15,_ := strconv.ParseFloat(fields[2],64) + return l1,l5,l15,nil +} + +// cpuCores возвращает число доступных логических CPU. +func cpuCores() int { + return runtimeNumCPU() +} + +// runtimeNumCPU — обёртка для получения числа CPU без CGO. +func runtimeNumCPU() int { + return int(numCPU()) +} + +// numCPU возвращает количество процессоров из /proc/cpuinfo (без вызова CGO). +func numCPU() uint64 { + f, err := os.Open("/proc/cpuinfo") + if err != nil { return 1 } + defer f.Close() + s := bufio.NewScanner(f) + var cnt uint64 + for s.Scan() { + if strings.HasPrefix(s.Text(), "processor") { cnt++ } + } + if cnt == 0 { cnt = 1 } + return cnt +} + +// collectCPU собирает базовые CPU-метрики: cores, usage_pct и load1/5/15. +func collectCPU(ctx context.Context) (map[string]any, error) { + usage, _ := cpuUsagePercent(300) + l1,l5,l15,_ := loadAvg() + cores := cpuCores() + return map[string]any{ + "cores": cores, + "usage_pct": usage, + "load1": l1, + "load5": l5, + "load15": l15, + }, nil +} + +// --- MEM функции из system коллектора --- + +// collectMem собирает метрики RAM и Swap из /proc/meminfo (байты и usage_pct). +func collectMem(ctx context.Context) (map[string]any, map[string]any, error) { + b, err := os.ReadFile("/proc/meminfo") + if err != nil { return nil, nil, err } + vals := map[string]uint64{} + s := bufio.NewScanner(strings.NewReader(string(b))) + for s.Scan() { + line := s.Text() + parts := strings.Split(line, ":") + if len(parts) < 2 { continue } + key := strings.TrimSpace(parts[0]) + num := strings.Fields(parts[1]) + if len(num) == 0 { continue } + v, _ := strconv.ParseUint(num[0], 10, 64) + vals[key] = v * 1024 // kB -> bytes + } + total := vals["MemTotal"] + free := vals["MemFree"] + vals["Buffers"] + vals["Cached"] + used := total - free + usage := pct(used, total) + // swap + sTotal := vals["SwapTotal"] + sFree := vals["SwapFree"] + sUsed := sTotal - sFree + sUsage := pct(sUsed, sTotal) + return map[string]any{ + "total": total, + "used": used, + "usage_pct": usage, + }, map[string]any{ + "total": sTotal, + "used": sUsed, + "usage_pct": sUsage, + }, nil +} + +// pct вычисляет процент от общего значения +func pct(part, total uint64) float64 { + if total == 0 { return 0 } + return float64(part) / float64(total) * 100.0 +} + +// --- NET функции из system коллектора --- + +// netCounters хранит суммарные байты интерфейса (из /proc/net/dev) +type netCounters struct{ rxBytes, txBytes uint64 } + +// readNetCounters читает /proc/net/dev и возвращает суммарные rx/tx байты по интерфейсам. +func readNetCounters() (map[string]netCounters, error) { + f, err := os.Open("/proc/net/dev") + if err != nil { return nil, err } + defer f.Close() + res := map[string]netCounters{} + s := bufio.NewScanner(f) + for i := 0; s.Scan(); i++ { + if i < 2 { continue } + line := strings.TrimSpace(s.Text()) + parts := strings.Split(line, ":") + if len(parts) != 2 { continue } + name := strings.TrimSpace(parts[0]) + fields := strings.Fields(parts[1]) + if len(fields) < 16 { continue } + rx,_ := strconv.ParseUint(fields[0],10,64) + tx,_ := strconv.ParseUint(fields[8],10,64) + res[name] = netCounters{rxBytes: rx, txBytes: tx} + } + return res, nil +} + +// ifaceSpeedMbps возвращает объявленную скорость интерфейса +func ifaceSpeedMbps(name string) int { + p := filepath.Join("/sys/class/net", name, "speed") + b, err := os.ReadFile(p) + if err != nil { return 0 } + n, _ := strconv.Atoi(strings.TrimSpace(string(b))) + return n +} + +// ifaceMAC возвращает MAC-адрес интерфейса. +func ifaceMAC(name string) string { + iface, err := net.InterfaceByName(name) + if err != nil { return "" } + return iface.HardwareAddr.String() +} + +// ifaceIPs возвращает список IPv4/IPv6 адресов интерфейса +func ifaceIPs(name string) []string { + iface, err := net.InterfaceByName(name) + if err != nil { return nil } + addrs, _ := iface.Addrs() + ips := []string{} + for _, a := range addrs { + ip, _, _ := net.ParseCIDR(a.String()) + if ip == nil { continue } + if ip.IsLoopback() { continue } + if v4 := ip.To4(); v4 != nil { + if v4[0] == 169 && v4[1] == 254 { continue } + ips = append(ips, v4.String()) + continue + } + if ip.IsLinkLocalUnicast() { continue } + ips = append(ips, ip.String()) + } + return ips +} + +// collectNet собирает метрики сетевых интерфейсов +func collectNet(ctx context.Context) (map[string]any, error) { + counters, err := readNetCounters() + if err != nil { return nil, err } + + interfaces := map[string]any{} + for name, cnt := range counters { + if strings.HasPrefix(name, "lo") { continue } + + speed := ifaceSpeedMbps(name) + mac := ifaceMAC(name) + ips := ifaceIPs(name) + + interfaces[name] = map[string]any{ + "rx_bytes": cnt.rxBytes, + "tx_bytes": cnt.txBytes, + "speed_mbps": speed, + "mac": mac, + "ips": ips, + } + } + + return interfaces, nil +} + +// --- DISK функции из system коллектора --- + +// collectDisks собирает метрики дисков +func collectDisks(ctx context.Context) (map[string]any, error) { + cmd := exec.CommandContext(ctx, "df", "-B1", "--output=source,target,fstype,size,used,avail,pcent") + output, err := cmd.Output() + if err != nil { return nil, err } + + disks := map[string]any{} + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + + for i, line := range lines { + if i == 0 { continue } // пропускаем заголовок + fields := strings.Fields(line) + if len(fields) < 7 { continue } + + device := fields[0] + mount := fields[1] + fstype := fields[2] + size, _ := strconv.ParseUint(fields[3], 10, 64) + used, _ := strconv.ParseUint(fields[4], 10, 64) + avail, _ := strconv.ParseUint(fields[5], 10, 64) + pcent := strings.TrimSuffix(fields[6], "%") + usage, _ := strconv.ParseFloat(pcent, 64) + + disks[device] = map[string]any{ + "mount": mount, + "fstype": fstype, + "size_bytes": size, + "used_bytes": used, + "avail_bytes": avail, + "usage_pct": usage, + } + } + + return disks, nil +} + +// --- TIME SYNC функции из system коллектора --- + +// collectTimeSync собирает информацию о синхронизации времени +func collectTimeSync(ctx context.Context) (map[string]any, error) { + result := map[string]any{} + + // Проверяем NTP + if exists("ntpq") { + cmd := exec.CommandContext(ctx, "ntpq", "-p") + output, err := cmd.Output() + if err == nil { + result["ntp_peers"] = strings.TrimSpace(string(output)) + } + } + + // Проверяем systemd-timesyncd + if exists("timedatectl") { + cmd := exec.CommandContext(ctx, "timedatectl", "status") + output, err := cmd.Output() + if err == nil { + result["timedatectl"] = strings.TrimSpace(string(output)) + } + } + + return result, nil +} + +// --- UPDATES функции из system коллектора --- + +// collectUpdates собирает информацию об обновлениях +func collectUpdates(ctx context.Context) (map[string]any, error) { + result := map[string]any{} + + // Проверяем apt (Debian/Ubuntu) + if exists("apt") { + cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable") + output, err := cmd.Output() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + updates := 0 + for _, line := range lines { + if strings.Contains(line, "upgradable") { + updates++ + } + } + result["apt_updates"] = updates + } + } + + // Проверяем yum (RHEL/CentOS) + if exists("yum") { + cmd := exec.CommandContext(ctx, "yum", "check-update", "--quiet") + output, err := cmd.Output() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + result["yum_updates"] = len(lines) + } + } + + return result, nil +} diff --git a/src/collectors/proxvmsystem/proxvmsystem_unsupported.go b/src/collectors/proxvmsystem/proxvmsystem_unsupported.go new file mode 100644 index 0000000..1c94951 --- /dev/null +++ b/src/collectors/proxvmsystem/proxvmsystem_unsupported.go @@ -0,0 +1,18 @@ +//go:build !linux + +package main + +// Автор: Сергей Антропов, сайт: https://devops.org.ru +// Коллектор proxvmsystem для неподдерживаемых платформ + +import ( + "context" +) + +// collectProxVMSystem возвращает пустой результат для неподдерживаемых платформ +func collectProxVMSystem(ctx context.Context) (map[string]any, error) { + return map[string]any{ + "collector_name": "proxvmsystem", + "error": "unsupported platform", + }, nil +}