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/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/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/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
|
fi
|
||||||
@# Убедимся, что скрипты исполняемые
|
@# Убедимся, что скрипты исполняемые
|
||||||
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
@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/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/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/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:
|
collectors-windows:
|
||||||
# Кросс-сборка коллекторов для Windows
|
# Кросс-сборка коллекторов для Windows
|
||||||
|
@ -5,7 +5,7 @@ mode: auto # stdout | kafka | auto
|
|||||||
log_level: info
|
log_level: info
|
||||||
|
|
||||||
kafka:
|
kafka:
|
||||||
enabled: false
|
enabled: true
|
||||||
brokers: ["10.29.91.4:9092"]
|
brokers: ["10.29.91.4:9092"]
|
||||||
topic: "sensus.metrics"
|
topic: "sensus.metrics"
|
||||||
client_id: "sensusagent"
|
client_id: "sensusagent"
|
||||||
@ -23,7 +23,7 @@ kafka:
|
|||||||
|
|
||||||
collectors:
|
collectors:
|
||||||
system:
|
system:
|
||||||
enabled: true
|
enabled: false
|
||||||
type: exec
|
type: exec
|
||||||
key: system
|
key: system
|
||||||
interval: "3600s"
|
interval: "3600s"
|
||||||
@ -55,7 +55,7 @@ collectors:
|
|||||||
exec: "./collectors/sample.sh"
|
exec: "./collectors/sample.sh"
|
||||||
platforms: [darwin, linux]
|
platforms: [darwin, linux]
|
||||||
hba:
|
hba:
|
||||||
enabled: true
|
enabled: false
|
||||||
type: exec
|
type: exec
|
||||||
key: hba
|
key: hba
|
||||||
interval: "3600s"
|
interval: "3600s"
|
||||||
@ -63,7 +63,7 @@ collectors:
|
|||||||
exec: "./collectors/hba"
|
exec: "./collectors/hba"
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
sensors:
|
sensors:
|
||||||
enabled: true
|
enabled: false
|
||||||
type: exec
|
type: exec
|
||||||
key: sensors
|
key: sensors
|
||||||
interval: "3600s"
|
interval: "3600s"
|
||||||
@ -79,7 +79,7 @@ collectors:
|
|||||||
exec: "./collectors/docker"
|
exec: "./collectors/docker"
|
||||||
platforms: [darwin, linux]
|
platforms: [darwin, linux]
|
||||||
gpu:
|
gpu:
|
||||||
enabled: true
|
enabled: false
|
||||||
type: exec
|
type: exec
|
||||||
key: gpu
|
key: gpu
|
||||||
interval: "3600s"
|
interval: "3600s"
|
||||||
@ -94,14 +94,21 @@ collectors:
|
|||||||
timeout: "60s"
|
timeout: "60s"
|
||||||
exec: "./collectors/kubernetes"
|
exec: "./collectors/kubernetes"
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
|
proxnode:
|
||||||
|
enabled: true
|
||||||
|
type: exec
|
||||||
|
key: proxnode
|
||||||
|
interval: "1800s"
|
||||||
|
timeout: "30s"
|
||||||
|
exec: "./collectors/proxnode"
|
||||||
|
platforms: [linux]
|
||||||
proxcluster:
|
proxcluster:
|
||||||
enabled: true
|
enabled: true
|
||||||
type: exec
|
type: exec
|
||||||
key: proxcluster
|
key: proxcluster
|
||||||
interval: "1800s"
|
interval: "1800s"
|
||||||
timeout: "30s"
|
timeout: "600s"
|
||||||
exec: "./collectors/proxcluster"
|
exec: "./collectors/proxcluster"
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,4 +5,4 @@
|
|||||||
#videotest7 ansible_host=10.13.37.186 ansible_user=devops
|
#videotest7 ansible_host=10.13.37.186 ansible_user=devops
|
||||||
#videotest8 ansible_host=10.13.37.187 ansible_user=devops
|
#videotest8 ansible_host=10.13.37.187 ansible_user=devops
|
||||||
pnode02 ansible_host=10.14.253.12 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 реализуется платформенно.
|
// collectDocker реализуется платформенно.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -25,6 +28,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = enc.Encode(data)
|
||||||
|
@ -16,6 +16,9 @@ import (
|
|||||||
// collectGPU реализуется платформенно.
|
// collectGPU реализуется платформенно.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -27,6 +30,14 @@ func main() {
|
|||||||
fmt.Println("{\"gpu\":[]}")
|
fmt.Println("{\"gpu\":[]}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
// Если ключ gpu отсутствует, нормализуем к пустому массиву
|
// Если ключ gpu отсутствует, нормализуем к пустому массиву
|
||||||
if _, ok := data["gpu"]; !ok {
|
if _, ok := data["gpu"]; !ok {
|
||||||
data["gpu"] = []any{}
|
data["gpu"] = []any{}
|
||||||
|
@ -16,6 +16,9 @@ import (
|
|||||||
// collectHBA реализуется в файлах с билд-тегами под конкретные ОС.
|
// collectHBA реализуется в файлах с билд-тегами под конкретные ОС.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -26,6 +29,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = enc.Encode(data)
|
||||||
|
@ -16,6 +16,9 @@ import (
|
|||||||
// collectKubernetes реализуется платформенно.
|
// collectKubernetes реализуется платформенно.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 12*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 12*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -26,6 +29,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = enc.Encode(data)
|
||||||
|
@ -15,6 +15,9 @@ import (
|
|||||||
// collectInfo реализуется в файлах с билд-тегами.
|
// collectInfo реализуется в файлах с билд-тегами.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -24,6 +27,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = enc.Encode(data)
|
||||||
|
@ -13,6 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -23,6 +26,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = 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 реализуется платформенно.
|
// collectSensors реализуется платформенно.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -25,6 +28,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = enc.Encode(data)
|
||||||
|
@ -16,6 +16,9 @@ import (
|
|||||||
// collectSystem реализуется в файлах с билд-тегами под конкретные ОС.
|
// collectSystem реализуется в файлах с билд-тегами под конкретные ОС.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
// Таймаут можно переопределить окружением COLLECTOR_TIMEOUT
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 8*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
@ -26,6 +29,14 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
// Добавляем время выполнения в результат
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
_ = enc.Encode(data)
|
_ = enc.Encode(data)
|
||||||
|
@ -15,6 +15,9 @@ import (
|
|||||||
// collectUptime реализуется в файлах с билд-тегами под конкретные ОС.
|
// collectUptime реализуется в файлах с билд-тегами под конкретные ОС.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Засекаем время начала выполнения
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 5*time.Second)
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 5*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -24,10 +27,16 @@ func main() {
|
|||||||
fmt.Println("{}")
|
fmt.Println("{}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вычисляем время выполнения
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
out := map[string]any{
|
out := map[string]any{
|
||||||
"collector_name": "uptime",
|
"collector_name": "uptime",
|
||||||
"seconds": secs,
|
"seconds": secs,
|
||||||
"human": humanize(time.Duration(secs) * time.Second),
|
"human": humanize(time.Duration(secs) * time.Second),
|
||||||
|
"execution_time_ms": executionTime.Milliseconds(),
|
||||||
|
"execution_time_seconds": executionTime.Seconds(),
|
||||||
}
|
}
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
|
@ -31,6 +31,15 @@ type Output interface {
|
|||||||
type StdoutOutput struct{}
|
type StdoutOutput struct{}
|
||||||
|
|
||||||
func (s *StdoutOutput) Write(_ context.Context, data Payload) error {
|
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 := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
return enc.Encode(data)
|
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 {
|
if err := k.writer.WriteMessages(ctx, msg); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,11 +40,13 @@ func (r *Runner) RunOnce(ctx context.Context) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(c collector.Collector) {
|
go func(c collector.Collector) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
slog.Info("collector started", "name", c.Name(), "key", c.Key())
|
||||||
res, err := c.Collect(ctx)
|
res, err := c.Collect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("collector error", "name", c.Name(), "err", err)
|
slog.Warn("collector error", "name", c.Name(), "err", err)
|
||||||
res = map[string]any{}
|
res = map[string]any{}
|
||||||
}
|
}
|
||||||
|
slog.Info("collector completed", "name", c.Name(), "key", c.Key())
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
payload[c.Key()] = res
|
payload[c.Key()] = res
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
@ -80,12 +82,14 @@ func (r *Runner) RunContinuous(ctx context.Context) {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
// Немедленный первый запуск
|
// Немедленный первый запуск
|
||||||
runOnce := func() {
|
runOnce := func() {
|
||||||
|
slog.Info("collector started", "name", c.Name(), "key", c.Key())
|
||||||
res, err := c.Collect(ctx)
|
res, err := c.Collect(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("collector error", "name", c.Name(), "err", err)
|
slog.Warn("collector error", "name", c.Name(), "err", err)
|
||||||
res = map[string]any{}
|
res = map[string]any{}
|
||||||
}
|
}
|
||||||
if res == nil { res = map[string]any{} }
|
if res == nil { res = map[string]any{} }
|
||||||
|
slog.Info("collector completed", "name", c.Name(), "key", c.Key())
|
||||||
select {
|
select {
|
||||||
case updates <- update{key: c.Key(), data: res, intervalSec: int(c.Interval().Seconds())}:
|
case updates <- update{key: c.Key(), data: res, intervalSec: int(c.Interval().Seconds())}:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user