feat(gpu): новый коллектор GPU для Linux (nvidia-smi/rocm-smi); добавлен в сборку и config
This commit is contained in:
parent
2e87580e84
commit
10b79a14bb
4
Makefile
4
Makefile
@ -46,7 +46,7 @@ collectors:
|
||||
else \
|
||||
docker run --rm -v $$PWD:/workspace -w /workspace \
|
||||
-e GOOS=linux -e GOARCH=amd64 -e GOCACHE=/workspace/.cache/go-build -e GOMODCACHE=/workspace/.cache/go-mod golang:1.22 \
|
||||
sh -c "go mod tidy >/dev/null 2>&1 && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/uptime ./src/collectors/uptime && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/macos ./src/collectors/macos && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/system ./src/collectors/system && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/hba ./src/collectors/hba && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/sensors ./src/collectors/sensors && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/docker ./src/collectors/docker"; \
|
||||
sh -c "go mod tidy >/dev/null 2>&1 && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/uptime ./src/collectors/uptime && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/macos ./src/collectors/macos && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/system ./src/collectors/system && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/hba ./src/collectors/hba && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/sensors ./src/collectors/sensors && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/docker ./src/collectors/docker && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/gpu ./src/collectors/gpu"; \
|
||||
fi
|
||||
@# Убедимся, что скрипты исполняемые
|
||||
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
||||
@ -61,7 +61,7 @@ collectors-linux:
|
||||
# Кросс-сборка коллекторов для Linux
|
||||
@mkdir -p ./bin/agent/collectors .cache/go-build .cache/go-mod; \
|
||||
docker run --rm -v $$PWD:/workspace -w /workspace -e GOOS=linux -e GOARCH=amd64 -e GOCACHE=/workspace/.cache/go-build -e GOMODCACHE=/workspace/.cache/go-mod golang:1.22 \
|
||||
sh -c "go mod tidy >/dev/null 2>&1 && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/uptime ./src/collectors/uptime && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/macos ./src/collectors/macos && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/system ./src/collectors/system && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/hba ./src/collectors/hba && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/sensors ./src/collectors/sensors && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/docker ./src/collectors/docker"
|
||||
sh -c "go mod tidy >/dev/null 2>&1 && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/uptime ./src/collectors/uptime && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/macos ./src/collectors/macos && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/system ./src/collectors/system && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/hba ./src/collectors/hba && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/sensors ./src/collectors/sensors && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/docker ./src/collectors/docker && CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/gpu ./src/collectors/gpu"
|
||||
|
||||
collectors-windows:
|
||||
# Кросс-сборка коллекторов для Windows
|
||||
|
@ -69,6 +69,14 @@ collectors:
|
||||
timeout: "20s"
|
||||
exec: "./collectors/docker"
|
||||
platforms: [darwin, linux]
|
||||
gpu:
|
||||
enabled: true
|
||||
type: exec
|
||||
key: gpu
|
||||
interval: "30s"
|
||||
timeout: "8s"
|
||||
exec: "./collectors/gpu"
|
||||
platforms: [linux]
|
||||
|
||||
|
||||
|
||||
|
184
src/collectors/gpu/gpu_linux.go
Normal file
184
src/collectors/gpu/gpu_linux.go
Normal file
@ -0,0 +1,184 @@
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
// Автор: Сергей Антропов, сайт: https://devops.org.ru
|
||||
// Linux-реализация gpu: NVIDIA через nvidia-smi, AMD через rocm-smi.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// collectGPU возвращает данные в формате {"gpu": [ {...}, ... ]}
|
||||
func collectGPU(ctx context.Context) (map[string]any, error) {
|
||||
// Сначала пробуем NVIDIA
|
||||
if exists("nvidia-smi") {
|
||||
if arr := collectNvidia(ctx); len(arr) > 0 {
|
||||
return map[string]any{"gpu": arr}, nil
|
||||
}
|
||||
}
|
||||
// Затем AMD ROCm
|
||||
if exists("rocm-smi") {
|
||||
if arr := collectRocm(ctx); len(arr) > 0 {
|
||||
return map[string]any{"gpu": arr}, nil
|
||||
}
|
||||
}
|
||||
return map[string]any{"gpu": []any{}}, nil
|
||||
}
|
||||
|
||||
// collectNvidia парсит вывод nvidia-smi в csv,noheader,nounits
|
||||
func collectNvidia(ctx context.Context) []map[string]any {
|
||||
q := "nvidia-smi --query-gpu=index,name,driver_version,memory.total,memory.used,utilization.gpu,utilization.memory,temperature.gpu,power.draw --format=csv,noheader,nounits"
|
||||
out, err := run(ctx, "sh", "-c", q+" 2>/dev/null")
|
||||
if err != nil || strings.TrimSpace(out) == "" { return nil }
|
||||
lines := strings.Split(strings.TrimSpace(out), "\n")
|
||||
res := make([]map[string]any, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
parts := splitCSV(line)
|
||||
if len(parts) < 9 { continue }
|
||||
item := map[string]any{}
|
||||
item["id"] = atoi(parts[0])
|
||||
item["name"] = parts[1]
|
||||
item["driver_version"] = parts[2]
|
||||
item["mem_total_mb"] = atoi(parts[3])
|
||||
item["mem_used_mb"] = atoi(parts[4])
|
||||
item["gpu_util_pct"] = atoi(parts[5])
|
||||
item["mem_util_pct"] = atoi(parts[6])
|
||||
item["temperature_c"] = atoi(parts[7])
|
||||
item["power_watt"] = atof(parts[8])
|
||||
res = append(res, item)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// collectRocm пытается получить данные через rocm-smi в формате JSON
|
||||
func collectRocm(ctx context.Context) []map[string]any {
|
||||
// Предпочитаем JSON-вывод, чтобы избежать парсинга неструктурированного текста
|
||||
// Набор метрик: id, name, driver_version, vram total/used, gpu/mem util, temp, power
|
||||
out, err := run(ctx, "sh", "-c", "rocm-smi --showid --showproductname --showdriverversion --showmeminfo vram --showuse --showtemp --showpower --json 2>/dev/null")
|
||||
if err != nil || strings.TrimSpace(out) == "" { return nil }
|
||||
// Ответ обычно представляет собой JSON-объект с ключами по картам (card0, card1 ... или "card": {"0": {...}})
|
||||
// Используем динамический парсинг
|
||||
var m map[string]any
|
||||
if e := json.Unmarshal([]byte(out), &m); e != nil { return nil }
|
||||
// Попробуем разные формы структуры
|
||||
cards := []map[string]any{}
|
||||
if cardMap, ok := m["card"].(map[string]any); ok {
|
||||
for k, v := range cardMap {
|
||||
if obj, ok2 := v.(map[string]any); ok2 { cards = append(cards, normalizeRocmCard(k, obj)) }
|
||||
}
|
||||
} else {
|
||||
// некоторые версии отдают плоские ключи card0, card1
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(strings.ToLower(k), "card") {
|
||||
if obj, ok2 := v.(map[string]any); ok2 { cards = append(cards, normalizeRocmCard(strings.TrimPrefix(strings.ToLower(k), "card"), obj)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Удаляем пустые
|
||||
res := []map[string]any{}
|
||||
for _, c := range cards {
|
||||
if len(c) > 0 { res = append(res, c) }
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// normalizeRocmCard приводит карту полей rocm-smi к нашей схеме
|
||||
func normalizeRocmCard(idx string, obj map[string]any) map[string]any {
|
||||
item := map[string]any{}
|
||||
if i := atoi(strings.TrimSpace(idx)); i >= 0 { item["id"] = i }
|
||||
// Возможные ключи в разных версиях rocm-smi
|
||||
// Имя
|
||||
if v, ok := firstString(obj,
|
||||
"Card series", "Device SKU", "Product", "Card model", "card_model", "Card Model"); ok { item["name"] = v }
|
||||
// Версия драйвера
|
||||
if v, ok := firstString(obj, "Driver version", "driver_version"); ok { item["driver_version"] = v }
|
||||
// Память
|
||||
// Некоторые версии дают байты: "VRAM Total Memory (B)", "VRAM Used Memory (B)"
|
||||
if tStr, ok := firstString(obj, "VRAM Total Memory (B)", "vram_total"); ok {
|
||||
t := atoui64(tStr)
|
||||
if t > 0 { item["mem_total_mb"] = t / (1024 * 1024) }
|
||||
}
|
||||
if uStr, ok := firstString(obj, "VRAM Used Memory (B)", "vram_used"); ok {
|
||||
u := atoui64(uStr)
|
||||
if u > 0 { item["mem_used_mb"] = u / (1024 * 1024) }
|
||||
}
|
||||
// Загрузка ядра/памяти
|
||||
if v, ok := firstString(obj, "GPU use (%)", "GPU use", "gpu_use"); ok { item["gpu_util_pct"] = atoi(v) }
|
||||
if v, ok := firstString(obj, "GPU memory use (%)", "GPU memory use", "mem_use"); ok { item["mem_util_pct"] = atoi(v) }
|
||||
// Температура
|
||||
if v, ok := firstString(obj, "Temperature (Sensor die) (C)", "Temperature (Sensor edge) (C)", "temperature"); ok { item["temperature_c"] = atof(v) }
|
||||
// Питание
|
||||
if v, ok := firstString(obj, "Power (W)", "Average Graphics Package Power (W)", "power"); ok { item["power_watt"] = atof(v) }
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
// exists проверяет наличие бинаря в $PATH
|
||||
func exists(bin string) bool {
|
||||
_, err := exec.LookPath(bin)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// splitCSV аккуратно разделяет CSV без кавычек (nvidia-smi выводит простой csv)
|
||||
func splitCSV(line string) []string {
|
||||
parts := strings.Split(line, ",")
|
||||
for i := range parts { parts[i] = strings.TrimSpace(parts[i]) }
|
||||
return parts
|
||||
}
|
||||
|
||||
func atoi(s string) int {
|
||||
s = strings.TrimSpace(s)
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
||||
|
||||
func atoui64(s string) uint64 {
|
||||
s = strings.TrimSpace(s)
|
||||
// удаляем возможные единицы измерения
|
||||
s = strings.TrimSuffix(s, "B")
|
||||
v, _ := strconv.ParseUint(s, 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
func atof(s string) float64 {
|
||||
s = strings.TrimSpace(s)
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
}
|
||||
|
||||
func run(ctx context.Context, bin string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
b, err := cmd.Output()
|
||||
if err != nil { return "", err }
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// firstString возвращает первое непустое строковое представление значения по перечню ключей
|
||||
func firstString(m map[string]any, keys ...string) (string, bool) {
|
||||
for _, k := range keys {
|
||||
if v, ok := m[k]; ok {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
s := strings.TrimSpace(t)
|
||||
if s != "" { return s, true }
|
||||
case float64:
|
||||
// JSON числа парсятся в float64
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", t)), true
|
||||
case int, int32, int64, uint64, uint32, uint8:
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", t)), true
|
||||
case map[string]any:
|
||||
// Иногда значение вложено; попытаемся взять common поле value
|
||||
if s, ok2 := firstString(t, "value", "Value"); ok2 { return s, true }
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
|
16
src/collectors/gpu/gpu_unsupported.go
Normal file
16
src/collectors/gpu/gpu_unsupported.go
Normal file
@ -0,0 +1,16 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
// Автор: Сергей Антропов, сайт: https://devops.org.ru
|
||||
// Заглушка для неподдерживаемых платформ: возвращает пустой массив gpu.
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func collectGPU(ctx context.Context) (map[string]any, error) {
|
||||
return map[string]any{"gpu": []any{}}, nil
|
||||
}
|
||||
|
||||
|
48
src/collectors/gpu/main.go
Normal file
48
src/collectors/gpu/main.go
Normal file
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
// Автор: Сергей Антропов, сайт: https://devops.org.ru
|
||||
// Коллектор gpu. Собирает информацию о видеокартах NVIDIA (через nvidia-smi)
|
||||
// и AMD (через rocm-smi). Возвращает массив видеокарт под ключом "gpu".
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// collectGPU реализуется платформенно.
|
||||
|
||||
func main() {
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
data, err := collectGPU(ctx)
|
||||
if err != nil || data == nil {
|
||||
// Всегда возвращаем пустой блок gpu, если нет данных
|
||||
fmt.Println("{\"gpu\":[]}")
|
||||
return
|
||||
}
|
||||
// Если ключ gpu отсутствует, нормализуем к пустому массиву
|
||||
if _, ok := data["gpu"]; !ok {
|
||||
data["gpu"] = []any{}
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
_ = enc.Encode(data)
|
||||
}
|
||||
|
||||
// parseDurationOr парсит длительность из переменной окружения или возвращает дефолт
|
||||
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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user