feat: улучшения proxcluster коллектора и системы логирования
- Переписан proxcluster коллектор с асинхронным получением данных - Добавлена информация о loadavg для каждой ноды - Добавлена суммарная статистика кластера (CPU, память, VM, контейнеры) - Добавлено время выполнения во все коллекторы Go (execution_time_ms/seconds) - Улучшено логирование агента: * Логи запуска/завершения коллекторов * Информация о коллекторах в Kafka/stdout логах - Добавлен новый коллектор proxnode - Обновлен Makefile для сборки proxcluster коллектора - Исправлены типы данных в main.go файлах коллекторов
This commit is contained in:
parent
c3a81d963f
commit
8b8f26909c
6
Makefile
6
Makefile
@ -59,7 +59,8 @@ collectors:
|
||||
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 && \
|
||||
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/proxcluster ./src/collectors/proxcluster && \
|
||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode"; \
|
||||
fi
|
||||
@# Убедимся, что скрипты исполняемые
|
||||
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
||||
@ -85,7 +86,8 @@ collectors-linux:
|
||||
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 && \
|
||||
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/proxcluster ./src/collectors/proxcluster && \
|
||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode"
|
||||
|
||||
collectors-windows:
|
||||
# Кросс-сборка коллекторов для Windows
|
||||
|
@ -5,7 +5,7 @@ mode: auto # stdout | kafka | auto
|
||||
log_level: info
|
||||
|
||||
kafka:
|
||||
enabled: false
|
||||
enabled: true
|
||||
brokers: ["10.29.91.4:9092"]
|
||||
topic: "sensus.metrics"
|
||||
client_id: "sensusagent"
|
||||
@ -23,7 +23,7 @@ kafka:
|
||||
|
||||
collectors:
|
||||
system:
|
||||
enabled: true
|
||||
enabled: false
|
||||
type: exec
|
||||
key: system
|
||||
interval: "3600s"
|
||||
@ -55,7 +55,7 @@ collectors:
|
||||
exec: "./collectors/sample.sh"
|
||||
platforms: [darwin, linux]
|
||||
hba:
|
||||
enabled: true
|
||||
enabled: false
|
||||
type: exec
|
||||
key: hba
|
||||
interval: "3600s"
|
||||
@ -63,7 +63,7 @@ collectors:
|
||||
exec: "./collectors/hba"
|
||||
platforms: [linux]
|
||||
sensors:
|
||||
enabled: true
|
||||
enabled: false
|
||||
type: exec
|
||||
key: sensors
|
||||
interval: "3600s"
|
||||
@ -79,7 +79,7 @@ collectors:
|
||||
exec: "./collectors/docker"
|
||||
platforms: [darwin, linux]
|
||||
gpu:
|
||||
enabled: true
|
||||
enabled: false
|
||||
type: exec
|
||||
key: gpu
|
||||
interval: "3600s"
|
||||
@ -94,14 +94,21 @@ collectors:
|
||||
timeout: "60s"
|
||||
exec: "./collectors/kubernetes"
|
||||
platforms: [linux]
|
||||
proxnode:
|
||||
enabled: true
|
||||
type: exec
|
||||
key: proxnode
|
||||
interval: "1800s"
|
||||
timeout: "30s"
|
||||
exec: "./collectors/proxnode"
|
||||
platforms: [linux]
|
||||
proxcluster:
|
||||
enabled: true
|
||||
type: exec
|
||||
key: proxcluster
|
||||
interval: "1800s"
|
||||
timeout: "30s"
|
||||
timeout: "600s"
|
||||
exec: "./collectors/proxcluster"
|
||||
platforms: [linux]
|
||||
|
||||
|
||||
|
||||
|
@ -5,4 +5,4 @@
|
||||
#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
|
||||
#dbrain01 ansible_host=10.14.246.75 ansible_user=devops
|
||||
|
@ -15,6 +15,9 @@ import (
|
||||
// collectDocker реализуется платформенно.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -25,6 +28,14 @@ func main() {
|
||||
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)
|
||||
|
@ -16,6 +16,9 @@ import (
|
||||
// collectGPU реализуется платформенно.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -27,6 +30,14 @@ func main() {
|
||||
fmt.Println("{\"gpu\":[]}")
|
||||
return
|
||||
}
|
||||
|
||||
// Вычисляем время выполнения
|
||||
executionTime := time.Since(startTime)
|
||||
|
||||
// Добавляем время выполнения в результат
|
||||
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||
data["execution_time_seconds"] = executionTime.Seconds()
|
||||
|
||||
// Если ключ gpu отсутствует, нормализуем к пустому массиву
|
||||
if _, ok := data["gpu"]; !ok {
|
||||
data["gpu"] = []any{}
|
||||
|
@ -16,6 +16,9 @@ import (
|
||||
// collectHBA реализуется в файлах с билд-тегами под конкретные ОС.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -26,6 +29,14 @@ func main() {
|
||||
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)
|
||||
|
@ -16,6 +16,9 @@ import (
|
||||
// collectKubernetes реализуется платформенно.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 12*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -26,6 +29,14 @@ func main() {
|
||||
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)
|
||||
|
@ -15,6 +15,9 @@ import (
|
||||
// collectInfo реализуется в файлах с билд-тегами.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
@ -24,6 +27,14 @@ func main() {
|
||||
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)
|
||||
|
@ -13,6 +13,9 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -23,6 +26,14 @@ func main() {
|
||||
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)
|
||||
|
File diff suppressed because it is too large
Load Diff
54
src/collectors/proxnode/main.go
Normal file
54
src/collectors/proxnode/main.go
Normal file
@ -0,0 +1,54 @@
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
// Автор: Сергей Антропов, сайт: https://devops.org.ru
|
||||
// Коллектор proxnode - собирает информацию о Proxmox ноде
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
data, err := collectProxCluster(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
|
||||
}
|
965
src/collectors/proxnode/proxnode_linux.go
Normal file
965
src/collectors/proxnode/proxnode_linux.go
Normal file
@ -0,0 +1,965 @@
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
// Автор: Сергей Антропов, сайт: https://devops.org.ru
|
||||
// Коллектор proxcluster для Linux - собирает информацию о Proxmox кластере
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Структуры для работы с API Proxmox
|
||||
type ClusterInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Nodes int `json:"nodes"`
|
||||
Quorate int `json:"quorate"`
|
||||
Type string `json:"type"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type ClusterStatus struct {
|
||||
ID string `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Level string `json:"level"`
|
||||
Local int `json:"local"`
|
||||
Name string `json:"name"`
|
||||
NodeID int `json:"nodeid"`
|
||||
Online int `json:"online"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type NodeStatus struct {
|
||||
Node string `json:"node"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
MaxCPU int `json:"maxcpu"`
|
||||
Mem int64 `json:"mem"`
|
||||
MaxMem int64 `json:"maxmem"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type VM struct {
|
||||
Vmid interface{} `json:"vmid"` // Может быть int или string
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Mem int64 `json:"mem"`
|
||||
MaxMem int64 `json:"maxmem"`
|
||||
Cores int `json:"cpus"`
|
||||
MaxDisk int64 `json:"maxdisk"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Итоговая структура
|
||||
type ClusterData struct {
|
||||
CollectorName string `json:"collector_name"`
|
||||
Summary map[string]any `json:"summary"`
|
||||
Nodes []map[string]any `json:"nodes"`
|
||||
}
|
||||
|
||||
// collectProxCluster собирает информацию о локальном Proxmox сервере
|
||||
func collectProxCluster(ctx context.Context) (map[string]interface{}, error) {
|
||||
// Получаем имя локального сервера
|
||||
hostname := runSimple("hostname")
|
||||
debugLog("Local hostname: %s", hostname)
|
||||
|
||||
// Получаем статус кластера для получения информации о кластере
|
||||
clusterStatRaw, err := runPvesh("/cluster/status")
|
||||
if err != nil {
|
||||
debugLog("Failed to get cluster status: %v", err)
|
||||
// Возвращаем базовую структуру при ошибке
|
||||
return map[string]interface{}{
|
||||
"collector_name": "proxcluster",
|
||||
"summary": map[string]interface{}{
|
||||
"cluster_id": "unknown",
|
||||
"cluster_uuid": "unknown",
|
||||
"name": "unknown",
|
||||
"version": "unknown",
|
||||
"cluster_resources": map[string]interface{}{
|
||||
"cpu": map[string]int{
|
||||
"total_cores": 0,
|
||||
"online_cores": 0,
|
||||
},
|
||||
"memory": map[string]int64{
|
||||
"total_mb": 0,
|
||||
"used_mb": 0,
|
||||
},
|
||||
"nodes": map[string]int{
|
||||
"total": 0,
|
||||
"online": 0,
|
||||
},
|
||||
},
|
||||
"quorum": map[string]any{},
|
||||
"corosync": map[string]any{},
|
||||
},
|
||||
"nodes": []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var clusterStatusData []map[string]interface{}
|
||||
if err := json.Unmarshal(clusterStatRaw, &clusterStatusData); err != nil {
|
||||
debugLog("Failed to parse cluster status: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Извлекаем информацию о кластере (первый элемент)
|
||||
var clusterInfo ClusterInfo
|
||||
if len(clusterStatusData) > 0 && clusterStatusData[0]["type"] == "cluster" {
|
||||
clusterInfo.ID = getString(clusterStatusData[0], "id")
|
||||
clusterInfo.Name = getString(clusterStatusData[0], "name")
|
||||
clusterInfo.Nodes = getInt(clusterStatusData[0], "nodes")
|
||||
clusterInfo.Quorate = getInt(clusterStatusData[0], "quorate")
|
||||
clusterInfo.Type = getString(clusterStatusData[0], "type")
|
||||
clusterInfo.Version = getInt(clusterStatusData[0], "version")
|
||||
}
|
||||
|
||||
// Извлекаем информацию о всех нодах и находим локальную
|
||||
var clusterStat []ClusterStatus
|
||||
var localNode ClusterStatus
|
||||
for i := 1; i < len(clusterStatusData); i++ {
|
||||
if clusterStatusData[i]["type"] == "node" {
|
||||
node := ClusterStatus{
|
||||
ID: getString(clusterStatusData[i], "id"),
|
||||
IP: getString(clusterStatusData[i], "ip"),
|
||||
Level: getString(clusterStatusData[i], "level"),
|
||||
Local: getInt(clusterStatusData[i], "local"),
|
||||
Name: getString(clusterStatusData[i], "name"),
|
||||
NodeID: getInt(clusterStatusData[i], "nodeid"),
|
||||
Online: getInt(clusterStatusData[i], "online"),
|
||||
Type: getString(clusterStatusData[i], "type"),
|
||||
}
|
||||
clusterStat = append(clusterStat, node)
|
||||
|
||||
// Находим локальную ноду
|
||||
if node.Local == 1 {
|
||||
localNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("Cluster info: %+v", clusterInfo)
|
||||
debugLog("Cluster nodes: %+v", clusterStat)
|
||||
debugLog("Local node: %+v", localNode)
|
||||
|
||||
// Получаем cluster_uuid из corosync
|
||||
clusterUUID := runSimple("grep", "cluster_name", "/etc/pve/corosync.conf")
|
||||
if clusterUUID == "unknown" || clusterUUID == "" {
|
||||
clusterUUID = clusterInfo.Name // fallback на имя из API
|
||||
} else {
|
||||
// Извлекаем только значение после двоеточия
|
||||
parts := strings.SplitN(clusterUUID, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
clusterUUID = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Генерируем cluster_id через SHA
|
||||
clusterID := generateClusterID(clusterUUID)
|
||||
|
||||
|
||||
// Обрабатываем только локальную ноду
|
||||
nodes := []map[string]any{}
|
||||
|
||||
if localNode.Name != "" {
|
||||
debugLog("Processing local node: %s", localNode.Name)
|
||||
|
||||
// Получаем статус локальной ноды
|
||||
nodeStatRaw, err := runPvesh("/nodes/" + localNode.Name + "/status")
|
||||
var nodeStatusData map[string]interface{}
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(nodeStatRaw, &nodeStatusData); err != nil {
|
||||
debugLog("Failed to parse node status for %s: %v", localNode.Name, err)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to get node status for %s: %v", localNode.Name, err)
|
||||
}
|
||||
|
||||
debugLog("Node status data for %s: %+v", localNode.Name, nodeStatusData)
|
||||
|
||||
// Получаем VM
|
||||
vmRaw, err := runPvesh("/nodes/" + localNode.Name + "/qemu")
|
||||
var vms []VM
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(vmRaw, &vms); err != nil {
|
||||
debugLog("Failed to parse VMs for %s: %v", localNode.Name, err)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to get VMs for %s: %v", localNode.Name, err)
|
||||
}
|
||||
|
||||
// Получаем контейнеры
|
||||
ctRaw, err := runPvesh("/nodes/" + localNode.Name + "/lxc")
|
||||
var cts []VM
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(ctRaw, &cts); err != nil {
|
||||
debugLog("Failed to parse containers for %s: %v", localNode.Name, err)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to get containers for %s: %v", localNode.Name, err)
|
||||
}
|
||||
|
||||
// Генерируем node_uid через SHA
|
||||
nodeUID := generateNodeUID(clusterInfo.ID, strconv.Itoa(localNode.NodeID))
|
||||
|
||||
// Извлекаем данные из статуса ноды
|
||||
cpuUsage := getFloat64(nodeStatusData, "cpu") * 100
|
||||
// Если CPU usage из API равен 0, получаем реальные данные из top
|
||||
if cpuUsage == 0 {
|
||||
cpuUsage = getRealCPUUsage()
|
||||
}
|
||||
uptime := getInt64(nodeStatusData, "uptime")
|
||||
|
||||
// Извлекаем данные о памяти
|
||||
memoryUsed := int64(0)
|
||||
memoryTotal := int64(0)
|
||||
if memory, ok := nodeStatusData["memory"].(map[string]interface{}); ok {
|
||||
memoryUsed = getInt64(memory, "used")
|
||||
memoryTotal = getInt64(memory, "total")
|
||||
}
|
||||
|
||||
// Извлекаем данные о swap
|
||||
swapUsed := int64(0)
|
||||
if swap, ok := nodeStatusData["swap"].(map[string]interface{}); ok {
|
||||
swapUsed = getInt64(swap, "used")
|
||||
}
|
||||
|
||||
// Извлекаем данные о CPU из cpuinfo
|
||||
cpuCores := 0
|
||||
cpuSockets := 0
|
||||
cpuModel := "unknown"
|
||||
if cpuinfo, ok := nodeStatusData["cpuinfo"].(map[string]interface{}); ok {
|
||||
cpuCores = getInt(cpuinfo, "cores")
|
||||
cpuSockets = getInt(cpuinfo, "sockets")
|
||||
cpuModel = getString(cpuinfo, "model")
|
||||
}
|
||||
|
||||
// Извлекаем loadavg
|
||||
loadavg := []float64{0, 0, 0}
|
||||
if loadavgData, ok := nodeStatusData["loadavg"].([]interface{}); ok && len(loadavgData) >= 3 {
|
||||
for i := 0; i < 3; i++ {
|
||||
if val, ok := loadavgData[i].(string); ok {
|
||||
if f, err := strconv.ParseFloat(val, 64); err == nil {
|
||||
loadavg[i] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем corosync IP из конфигурации
|
||||
corosyncIPs := getCorosyncIP(localNode.Name)
|
||||
|
||||
// Создаем данные локальной ноды
|
||||
node := map[string]any{
|
||||
"cluster_name": clusterInfo.Name,
|
||||
"cluster_uuid": clusterUUID,
|
||||
"name": localNode.Name,
|
||||
"node_id": localNode.NodeID,
|
||||
"node_uid": nodeUID,
|
||||
"cluster_uid": clusterID,
|
||||
"online": localNode.Online == 1,
|
||||
"product_uuid": runSimple("dmidecode", "-s", "system-uuid"),
|
||||
"machine_id": runSimple("cat", "/etc/machine-id"),
|
||||
"corosync_ip": corosyncIPs,
|
||||
"real_ips": getRealIPs(),
|
||||
"nodes": map[string]int{
|
||||
"online": countOnline(clusterStat),
|
||||
"total": len(clusterStat),
|
||||
},
|
||||
"hardware": map[string]any{
|
||||
"cpu_cores": cpuCores,
|
||||
"cpu_model": cpuModel,
|
||||
"total_memory_mb": memoryTotal / 1024 / 1024,
|
||||
"sockets": cpuSockets,
|
||||
"threads": cpuCores * cpuSockets,
|
||||
"total_cpu_cores": cpuCores * cpuSockets,
|
||||
},
|
||||
"os": map[string]any{
|
||||
"kernel": getString(nodeStatusData, "kversion"),
|
||||
"pve_version": getString(nodeStatusData, "pveversion"),
|
||||
"uptime_human": formatUptime(uptime),
|
||||
"uptime_sec": uptime,
|
||||
},
|
||||
"resources_used": map[string]any{
|
||||
"cpu_usage_percent": cpuUsage,
|
||||
"memory_used_mb": memoryUsed / 1024 / 1024,
|
||||
"swap_used_mb": swapUsed / 1024 / 1024,
|
||||
"loadavg": loadavg,
|
||||
},
|
||||
"vm_summary": map[string]any{
|
||||
"total_vms": len(vms),
|
||||
"total_containers": len(cts),
|
||||
"running_vms": 0, // Будет заполнено ниже
|
||||
"running_containers": 0, // Будет заполнено ниже
|
||||
"stopped_containers": 0, // Будет заполнено ниже
|
||||
"stopped_vms": 0, // Будет заполнено ниже
|
||||
"template_vms": 0, // Будет заполнено ниже
|
||||
},
|
||||
"vm_resources_summary": map[string]any{
|
||||
"total_host_cores": cpuCores * cpuSockets,
|
||||
"host_vm_total_cores": 0, // Будет заполнено ниже
|
||||
"host_vm_used_cores": 0, // Будет заполнено ниже
|
||||
"kvm64_vm_total_cores": 0, // Будет заполнено ниже
|
||||
"kvm64_vm_used_cores": 0, // Будет заполнено ниже
|
||||
"total_vm_cores": 0, // Будет заполнено ниже
|
||||
"used_vm_cores": 0, // Будет заполнено ниже
|
||||
"total_vm_memory_mb": 0, // Будет заполнено ниже
|
||||
"used_vm_memory_mb": 0, // Будет заполнено ниже
|
||||
},
|
||||
"quorum": getQuorumInfo(),
|
||||
"version": runSimple("pveversion"),
|
||||
}
|
||||
|
||||
// Получаем данные из summarizeVMs
|
||||
vmSummaryData := summarizeVMs(vms, cts, cpuCores, cpuSockets, localNode.Name)
|
||||
|
||||
// Заполняем vm_summary
|
||||
node["vm_summary"].(map[string]any)["running_vms"] = vmSummaryData["running_vms"]
|
||||
node["vm_summary"].(map[string]any)["running_containers"] = vmSummaryData["running_containers"]
|
||||
node["vm_summary"].(map[string]any)["stopped_containers"] = vmSummaryData["stopped_containers"]
|
||||
node["vm_summary"].(map[string]any)["stopped_vms"] = vmSummaryData["stopped_vms"]
|
||||
node["vm_summary"].(map[string]any)["template_vms"] = vmSummaryData["template_vms"]
|
||||
|
||||
// Заполняем vm_resources_summary
|
||||
node["vm_resources_summary"].(map[string]any)["host_vm_total_cores"] = vmSummaryData["host_total_cores"]
|
||||
node["vm_resources_summary"].(map[string]any)["host_vm_used_cores"] = vmSummaryData["host_used_cores"]
|
||||
node["vm_resources_summary"].(map[string]any)["kvm64_vm_total_cores"] = vmSummaryData["kvm64_total_cores"]
|
||||
node["vm_resources_summary"].(map[string]any)["kvm64_vm_used_cores"] = vmSummaryData["kvm64_used_cores"]
|
||||
node["vm_resources_summary"].(map[string]any)["total_vm_cores"] = vmSummaryData["total_cpu_cores"]
|
||||
node["vm_resources_summary"].(map[string]any)["used_vm_cores"] = vmSummaryData["used_cpu_cores"]
|
||||
node["vm_resources_summary"].(map[string]any)["total_vm_memory_mb"] = vmSummaryData["total_memory_mb"]
|
||||
node["vm_resources_summary"].(map[string]any)["used_vm_memory_mb"] = vmSummaryData["used_memory_mb"]
|
||||
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
// Возвращаем структуру в новом формате
|
||||
resultMap := map[string]interface{}{
|
||||
"collector_name": "proxnode",
|
||||
"node": nodes,
|
||||
}
|
||||
|
||||
debugLog("Final result: %+v", resultMap)
|
||||
return resultMap, nil
|
||||
}
|
||||
|
||||
// runPvesh запускает pvesh get <path> и возвращает JSON
|
||||
func runPvesh(path string) ([]byte, error) {
|
||||
cmd := exec.Command("pvesh", "get", path, "--output-format", "json")
|
||||
return cmd.Output()
|
||||
}
|
||||
|
||||
// countOnline считает количество онлайн нод
|
||||
func countOnline(stat []ClusterStatus) int {
|
||||
c := 0
|
||||
for _, n := range stat {
|
||||
if n.Online == 1 {
|
||||
c++
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// runSimple выполняет простую команду и возвращает результат
|
||||
func runSimple(cmd string, args ...string) string {
|
||||
out, err := exec.Command(cmd, args...).Output()
|
||||
if err != nil {
|
||||
debugLog("Failed to run command %s %v: %v", cmd, args, err)
|
||||
return "unknown"
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
|
||||
// readLoadavg читает load average из /proc/loadavg
|
||||
func readLoadavg() []float64 {
|
||||
raw := runSimple("cat", "/proc/loadavg")
|
||||
parts := strings.Fields(raw)
|
||||
var res []float64
|
||||
for i := 0; i < 3 && i < len(parts); i++ {
|
||||
var f float64
|
||||
if _, err := fmt.Sscanf(parts[i], "%f", &f); err == nil {
|
||||
res = append(res, f)
|
||||
} else {
|
||||
res = append(res, 0.0)
|
||||
}
|
||||
}
|
||||
// Дополняем до 3 элементов если не хватает
|
||||
for len(res) < 3 {
|
||||
res = append(res, 0.0)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// runCpuModel получает модель CPU
|
||||
func runCpuModel() string {
|
||||
raw := runSimple("grep", "model name", "/proc/cpuinfo")
|
||||
if raw == "" {
|
||||
return "unknown"
|
||||
}
|
||||
// Берем только первую строку
|
||||
lines := strings.Split(raw, "\n")
|
||||
if len(lines) > 0 {
|
||||
parts := strings.SplitN(lines[0], ":", 2)
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// summarizeVMs создает сводку по VM и контейнерам
|
||||
func summarizeVMs(vms []VM, cts []VM, hostCores int, hostSockets int, nodeName string) map[string]any {
|
||||
totalVMs := len(vms)
|
||||
runningVMs := 0
|
||||
stoppedVMs := 0
|
||||
templateVMs := 0
|
||||
cpuCores := 0
|
||||
usedCpuCores := 0
|
||||
hostUsedCores := 0
|
||||
hostTotalCores := 0
|
||||
kvm64TotalCores := 0
|
||||
kvm64UsedCores := 0
|
||||
memUsed := int64(0)
|
||||
memTotal := int64(0)
|
||||
|
||||
for _, v := range vms {
|
||||
// Проверяем, является ли VM template
|
||||
if isTemplateVM(v.Vmid, nodeName) {
|
||||
templateVMs++
|
||||
continue // Пропускаем template VM в подсчете ресурсов
|
||||
}
|
||||
|
||||
if v.Status == "running" {
|
||||
runningVMs++
|
||||
|
||||
// Проверяем тип CPU для запущенных VM
|
||||
if isHostCPU(v.Vmid, nodeName) {
|
||||
hostUsedCores += v.Cores // Ядра типа "host"
|
||||
} else {
|
||||
usedCpuCores += v.Cores // Ядра типа "kvm64", "kvm32" и т.д.
|
||||
kvm64UsedCores += v.Cores // Все не-host типы считаем kvm64
|
||||
}
|
||||
} else {
|
||||
// Считаем stopped VM только если это не template
|
||||
stoppedVMs++
|
||||
}
|
||||
|
||||
// Подсчитываем общие ядра для всех VM (включенных и выключенных, но не template)
|
||||
cpuCores += v.Cores
|
||||
if isHostCPU(v.Vmid, nodeName) {
|
||||
hostTotalCores += v.Cores // Общие ядра типа "host"
|
||||
} else {
|
||||
// Все остальные типы CPU (kvm64, kvm32 и т.д.) считаем kvm64
|
||||
kvm64TotalCores += v.Cores
|
||||
}
|
||||
|
||||
memUsed += v.Mem
|
||||
memTotal += v.MaxMem
|
||||
}
|
||||
|
||||
totalCTs := len(cts)
|
||||
runningCTs := 0
|
||||
stoppedCTs := 0
|
||||
for _, c := range cts {
|
||||
if c.Status == "running" {
|
||||
runningCTs++
|
||||
hostUsedCores += c.Cores // Контейнеры всегда используют host ядра
|
||||
} else {
|
||||
stoppedCTs++
|
||||
}
|
||||
cpuCores += c.Cores
|
||||
hostTotalCores += c.Cores // Контейнеры всегда используют host ядра
|
||||
memUsed += c.Mem
|
||||
memTotal += c.MaxMem
|
||||
}
|
||||
|
||||
// Вычисляем физические host ядра
|
||||
physicalHostCores := hostCores * hostSockets
|
||||
|
||||
return map[string]any{
|
||||
"total_vms": totalVMs,
|
||||
"running_vms": runningVMs,
|
||||
"stopped_vms": stoppedVMs,
|
||||
"template_vms": templateVMs, // Количество template VM
|
||||
"total_containers": totalCTs,
|
||||
"running_containers": runningCTs,
|
||||
"stopped_containers": stoppedCTs,
|
||||
"total_cpu_cores": cpuCores,
|
||||
"total_memory_mb": memTotal / 1024 / 1024,
|
||||
"used_cpu_cores": usedCpuCores,
|
||||
"used_memory_mb": memUsed / 1024 / 1024,
|
||||
"host_total_cores": hostTotalCores, // Общие ядра VM с типом "host" + контейнеры (без template)
|
||||
"host_used_cores": hostUsedCores, // Используемые ядра VM с типом "host" + контейнеры
|
||||
"kvm64_total_cores": kvm64TotalCores, // Общие ядра VM с типом "kvm64" (без template)
|
||||
"kvm64_used_cores": kvm64UsedCores, // Используемые ядра VM с типом "kvm64"
|
||||
"physical_host_cores": physicalHostCores, // Физические ядра хоста
|
||||
}
|
||||
}
|
||||
|
||||
// generateClusterID создает уникальный ID кластера на основе UUID
|
||||
func generateClusterID(clusterUUID string) string {
|
||||
if clusterUUID == "" {
|
||||
clusterUUID = "default-cluster-uuid"
|
||||
}
|
||||
hash := sha256.Sum256([]byte(clusterUUID))
|
||||
return hex.EncodeToString(hash[:])[:16] // первые 16 символов
|
||||
}
|
||||
|
||||
// generateNodeUID создает уникальный ID ноды
|
||||
func generateNodeUID(clusterUUID, nodeID string) string {
|
||||
base := clusterUUID + ":" + nodeID
|
||||
hash := sha256.Sum256([]byte(base))
|
||||
return hex.EncodeToString(hash[:])[:16]
|
||||
}
|
||||
|
||||
// getString извлекает строку из map[string]interface{}
|
||||
func getString(data map[string]interface{}, key string) string {
|
||||
if val, ok := data[key]; ok {
|
||||
if str, ok := val.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getInt извлекает int из map[string]interface{}
|
||||
func getInt(data map[string]interface{}, key string) int {
|
||||
if val, ok := data[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return v
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getInt64 извлекает int64 из map[string]interface{}
|
||||
func getInt64(data map[string]interface{}, key string) int64 {
|
||||
if val, ok := data[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return int64(v)
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return int64(v)
|
||||
case string:
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getFloat64 извлекает float64 из map[string]interface{}
|
||||
func getFloat64(data map[string]interface{}, key string) float64 {
|
||||
if val, ok := data[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
return v
|
||||
case int:
|
||||
return float64(v)
|
||||
case int64:
|
||||
return float64(v)
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// getRealIPs получает реальные IP адреса интерфейсов (исключая corosync IP)
|
||||
func getRealIPs() []string {
|
||||
raw := runSimple("ip", "addr", "show")
|
||||
lines := strings.Split(raw, "\n")
|
||||
var ips []string
|
||||
|
||||
// Получаем corosync IP для исключения
|
||||
corosyncIPs := getCorosyncIPs()
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "inet ") && !strings.Contains(line, "127.0.0.1") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
ip := strings.Split(parts[1], "/")[0] // убираем /24, /25 и т.д.
|
||||
|
||||
// Проверяем, не является ли это corosync IP
|
||||
isCorosyncIP := false
|
||||
for _, corosyncIP := range corosyncIPs {
|
||||
if ip == corosyncIP {
|
||||
isCorosyncIP = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isCorosyncIP {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// isHostCPU проверяет, использует ли VM host CPU
|
||||
func isHostCPU(vmid interface{}, nodeName string) bool {
|
||||
// Преобразуем vmid в строку
|
||||
var vmidStr string
|
||||
switch v := vmid.(type) {
|
||||
case int:
|
||||
vmidStr = strconv.Itoa(v)
|
||||
case string:
|
||||
vmidStr = v
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
// Получаем конфигурацию VM
|
||||
raw := runSimple("pvesh", "get", "/nodes/"+nodeName+"/qemu/"+vmidStr+"/config", "--output-format", "json")
|
||||
if raw == "unknown" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Парсим JSON для получения типа CPU
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(raw), &config); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем тип CPU
|
||||
if cpuType, ok := config["cpu"].(string); ok {
|
||||
return cpuType == "host"
|
||||
}
|
||||
|
||||
// По умолчанию считаем kvm64 (не host)
|
||||
return false
|
||||
}
|
||||
|
||||
// isTemplateVM проверяет, является ли VM template
|
||||
func isTemplateVM(vmid interface{}, nodeName string) bool {
|
||||
// Преобразуем vmid в строку
|
||||
var vmidStr string
|
||||
switch v := vmid.(type) {
|
||||
case int:
|
||||
vmidStr = strconv.Itoa(v)
|
||||
case string:
|
||||
vmidStr = v
|
||||
case float64:
|
||||
vmidStr = strconv.Itoa(int(v))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
// Получаем конфигурацию VM
|
||||
raw := runSimple("pvesh", "get", "/nodes/"+nodeName+"/qemu/"+vmidStr+"/config", "--output-format", "json")
|
||||
if raw == "unknown" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Парсим JSON для получения template флага
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(raw), &config); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем template флаг (может быть "template" или "templates")
|
||||
if template, ok := config["template"]; ok {
|
||||
if templateInt, ok := template.(float64); ok {
|
||||
return templateInt == 1
|
||||
}
|
||||
if templateInt, ok := template.(int); ok {
|
||||
return templateInt == 1
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем templates флаг
|
||||
if templates, ok := config["templates"]; ok {
|
||||
if templatesInt, ok := templates.(float64); ok {
|
||||
return templatesInt == 1
|
||||
}
|
||||
if templatesInt, ok := templates.(int); ok {
|
||||
return templatesInt == 1
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// formatUptime форматирует uptime в человеческий вид
|
||||
func formatUptime(seconds int64) string {
|
||||
days := seconds / 86400
|
||||
hours := (seconds % 86400) / 3600
|
||||
minutes := (seconds % 3600) / 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%d days, %d hours, %d minutes", days, hours, minutes)
|
||||
} else if hours > 0 {
|
||||
return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
|
||||
} else {
|
||||
return fmt.Sprintf("%d minutes", minutes)
|
||||
}
|
||||
}
|
||||
|
||||
// getRealCPUUsage получает реальное использование CPU из top
|
||||
func getRealCPUUsage() float64 {
|
||||
// Получаем данные из top
|
||||
raw := runSimple("top", "-bn1")
|
||||
lines := strings.Split(raw, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "%Cpu(s):") {
|
||||
// Парсим строку типа: %Cpu(s): 6.5 us, 7.5 sy, 0.0 ni, 59.3 id, 26.5 wa, 0.0 hi, 0.2 si, 0.0 st
|
||||
parts := strings.Split(line, ",")
|
||||
us := 0.0
|
||||
sy := 0.0
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.Contains(part, "us") {
|
||||
fmt.Sscanf(part, "%f us", &us)
|
||||
} else if strings.Contains(part, "sy") {
|
||||
fmt.Sscanf(part, "%f sy", &sy)
|
||||
}
|
||||
}
|
||||
|
||||
return us + sy // user + system = общее использование CPU
|
||||
}
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// getCorosyncInfo получает информацию из corosync.conf
|
||||
func getCorosyncInfo() map[string]any {
|
||||
corosyncInfo := map[string]any{
|
||||
"cluster_name": "unknown",
|
||||
"transport": "unknown",
|
||||
"nodes": []map[string]any{},
|
||||
}
|
||||
|
||||
// Читаем corosync.conf
|
||||
raw := runSimple("cat", "/etc/pve/corosync.conf")
|
||||
if raw == "unknown" {
|
||||
return corosyncInfo
|
||||
}
|
||||
|
||||
lines := strings.Split(raw, "\n")
|
||||
var currentNode map[string]any
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Парсим cluster_name
|
||||
if strings.HasPrefix(line, "cluster_name:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
corosyncInfo["cluster_name"] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Парсим transport
|
||||
if strings.HasPrefix(line, "transport:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
corosyncInfo["transport"] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Парсим ноды
|
||||
if line == "node {" {
|
||||
currentNode = map[string]any{}
|
||||
} else if line == "}" && currentNode != nil {
|
||||
corosyncInfo["nodes"] = append(corosyncInfo["nodes"].([]map[string]any), currentNode)
|
||||
currentNode = nil
|
||||
} else if currentNode != nil {
|
||||
if strings.HasPrefix(line, "name:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
currentNode["name"] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "nodeid:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
if id, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||
currentNode["nodeid"] = id
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(line, "ring0_addr:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
currentNode["ring0_addr"] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "ring1_addr:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
currentNode["ring1_addr"] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return corosyncInfo
|
||||
}
|
||||
|
||||
// getQuorumInfo получает информацию о quorum из pvecm status
|
||||
func getQuorumInfo() map[string]any {
|
||||
quorumInfo := map[string]any{
|
||||
"quorate": false,
|
||||
"members": 0,
|
||||
"total_votes": 0,
|
||||
"expected_votes": 0,
|
||||
}
|
||||
|
||||
// Получаем информацию из pvecm status
|
||||
raw := runSimple("pvecm", "status")
|
||||
if raw == "unknown" {
|
||||
return quorumInfo
|
||||
}
|
||||
|
||||
lines := strings.Split(raw, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if strings.Contains(line, "Quorate:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
quorumInfo["quorate"] = strings.TrimSpace(parts[1]) == "Yes"
|
||||
}
|
||||
} else if strings.Contains(line, "Expected votes:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||
quorumInfo["expected_votes"] = votes
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(line, "Total votes:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
if votes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||
quorumInfo["total_votes"] = votes
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(line, "Nodes:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
if nodes, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||
quorumInfo["members"] = nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return quorumInfo
|
||||
}
|
||||
|
||||
// getCorosyncIP получает corosync IP для конкретной ноды (возвращает оба IP)
|
||||
func getCorosyncIP(nodeName string) []string {
|
||||
// Читаем corosync.conf
|
||||
raw := runSimple("cat", "/etc/pve/corosync.conf")
|
||||
if raw == "unknown" {
|
||||
return []string{"unknown"}
|
||||
}
|
||||
|
||||
lines := strings.Split(raw, "\n")
|
||||
var currentNode string
|
||||
var ring0Addr string
|
||||
var ring1Addr string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "node {" {
|
||||
currentNode = ""
|
||||
ring0Addr = ""
|
||||
ring1Addr = ""
|
||||
} else if line == "}" && currentNode != "" {
|
||||
if currentNode == nodeName {
|
||||
ips := []string{ring0Addr}
|
||||
if ring1Addr != "" {
|
||||
ips = append(ips, ring1Addr)
|
||||
}
|
||||
return ips
|
||||
}
|
||||
currentNode = ""
|
||||
ring0Addr = ""
|
||||
ring1Addr = ""
|
||||
} else if strings.HasPrefix(line, "name:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
currentNode = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "ring0_addr:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
ring0Addr = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else if strings.HasPrefix(line, "ring1_addr:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
ring1Addr = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []string{"unknown"}
|
||||
}
|
||||
|
||||
// getCorosyncIPs получает все corosync IP адреса
|
||||
func getCorosyncIPs() []string {
|
||||
var ips []string
|
||||
|
||||
// Читаем corosync.conf
|
||||
raw := runSimple("cat", "/etc/pve/corosync.conf")
|
||||
if raw == "unknown" {
|
||||
return ips
|
||||
}
|
||||
|
||||
lines := strings.Split(raw, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if strings.HasPrefix(line, "ring0_addr:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
ip := strings.TrimSpace(parts[1])
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
} else if strings.HasPrefix(line, "ring1_addr:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) > 1 {
|
||||
ip := strings.TrimSpace(parts[1])
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
// debugLog выводит отладочную информацию (только если установлена переменная окружения DEBUG=1)
|
||||
func debugLog(format string, args ...interface{}) {
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", args...)
|
||||
}
|
||||
}
|
12
src/collectors/proxnode/proxnode_unsupported.go
Normal file
12
src/collectors/proxnode/proxnode_unsupported.go
Normal file
@ -0,0 +1,12 @@
|
||||
// +build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Платформа не поддерживается
|
||||
os.Exit(1)
|
||||
}
|
@ -15,6 +15,9 @@ import (
|
||||
// collectSensors реализуется платформенно.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -25,6 +28,14 @@ func main() {
|
||||
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)
|
||||
|
@ -16,6 +16,9 @@ import (
|
||||
// collectSystem реализуется в файлах с билд-тегами под конкретные ОС.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@ -26,6 +29,14 @@ func main() {
|
||||
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)
|
||||
|
@ -15,6 +15,9 @@ import (
|
||||
// collectUptime реализуется в файлах с билд-тегами под конкретные ОС.
|
||||
|
||||
func main() {
|
||||
// Засекаем время начала выполнения
|
||||
startTime := time.Now()
|
||||
|
||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
@ -24,10 +27,16 @@ func main() {
|
||||
fmt.Println("{}")
|
||||
return
|
||||
}
|
||||
|
||||
// Вычисляем время выполнения
|
||||
executionTime := time.Since(startTime)
|
||||
|
||||
out := map[string]any{
|
||||
"collector_name": "uptime",
|
||||
"seconds": secs,
|
||||
"human": humanize(time.Duration(secs) * time.Second),
|
||||
"execution_time_ms": executionTime.Milliseconds(),
|
||||
"execution_time_seconds": executionTime.Seconds(),
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
@ -31,6 +31,15 @@ type Output interface {
|
||||
type StdoutOutput struct{}
|
||||
|
||||
func (s *StdoutOutput) Write(_ context.Context, data Payload) error {
|
||||
// Извлекаем имена коллекторов из payload для логирования
|
||||
collectorNames := make([]string, 0, len(data))
|
||||
for key := range data {
|
||||
collectorNames = append(collectorNames, key)
|
||||
}
|
||||
|
||||
// Логируем вывод в stdout с информацией о коллекторах
|
||||
slog.Info("stdout output", "collectors", collectorNames)
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
return enc.Encode(data)
|
||||
@ -137,8 +146,15 @@ func (k *KafkaOutput) Write(ctx context.Context, data Payload) error {
|
||||
if err := k.writer.WriteMessages(ctx, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
// Логируем успешную отправку
|
||||
slog.Info("kafka message sent", "topic", k.topic, "bytes", len(b))
|
||||
|
||||
// Извлекаем имена коллекторов из payload для логирования
|
||||
collectorNames := make([]string, 0, len(data))
|
||||
for key := range data {
|
||||
collectorNames = append(collectorNames, key)
|
||||
}
|
||||
|
||||
// Логируем успешную отправку с информацией о коллекторах
|
||||
slog.Info("kafka message sent", "topic", k.topic, "bytes", len(b), "collectors", collectorNames)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -40,11 +40,13 @@ func (r *Runner) RunOnce(ctx context.Context) {
|
||||
wg.Add(1)
|
||||
go func(c collector.Collector) {
|
||||
defer wg.Done()
|
||||
slog.Info("collector started", "name", c.Name(), "key", c.Key())
|
||||
res, err := c.Collect(ctx)
|
||||
if err != nil {
|
||||
slog.Warn("collector error", "name", c.Name(), "err", err)
|
||||
res = map[string]any{}
|
||||
}
|
||||
slog.Info("collector completed", "name", c.Name(), "key", c.Key())
|
||||
mu.Lock()
|
||||
payload[c.Key()] = res
|
||||
mu.Unlock()
|
||||
@ -80,12 +82,14 @@ func (r *Runner) RunContinuous(ctx context.Context) {
|
||||
defer wg.Done()
|
||||
// Немедленный первый запуск
|
||||
runOnce := func() {
|
||||
slog.Info("collector started", "name", c.Name(), "key", c.Key())
|
||||
res, err := c.Collect(ctx)
|
||||
if err != nil {
|
||||
slog.Warn("collector error", "name", c.Name(), "err", err)
|
||||
res = map[string]any{}
|
||||
}
|
||||
if res == nil { res = map[string]any{} }
|
||||
slog.Info("collector completed", "name", c.Name(), "key", c.Key())
|
||||
select {
|
||||
case updates <- update{key: c.Key(), data: res, intervalSec: int(c.Interval().Seconds())}:
|
||||
case <-ctx.Done():
|
||||
|
Loading…
x
Reference in New Issue
Block a user