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 \
|
else \
|
||||||
docker run --rm -v $$PWD:/workspace -w /workspace \
|
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 \
|
-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
|
fi
|
||||||
@# Убедимся, что скрипты исполняемые
|
@# Убедимся, что скрипты исполняемые
|
||||||
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
||||||
@ -61,7 +61,7 @@ collectors-linux:
|
|||||||
# Кросс-сборка коллекторов для Linux
|
# Кросс-сборка коллекторов для Linux
|
||||||
@mkdir -p ./bin/agent/collectors .cache/go-build .cache/go-mod; \
|
@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 \
|
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:
|
collectors-windows:
|
||||||
# Кросс-сборка коллекторов для Windows
|
# Кросс-сборка коллекторов для Windows
|
||||||
|
@ -69,6 +69,14 @@ collectors:
|
|||||||
timeout: "20s"
|
timeout: "20s"
|
||||||
exec: "./collectors/docker"
|
exec: "./collectors/docker"
|
||||||
platforms: [darwin, linux]
|
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