feat: улучшения proxcluster коллектора и системы логирования

- Переписан proxcluster коллектор с асинхронным получением данных
- Добавлена информация о loadavg для каждой ноды
- Добавлена суммарная статистика кластера (CPU, память, VM, контейнеры)
- Добавлено время выполнения во все коллекторы Go (execution_time_ms/seconds)
- Улучшено логирование агента:
  * Логи запуска/завершения коллекторов
  * Информация о коллекторах в Kafka/stdout логах
- Добавлен новый коллектор proxnode
- Обновлен Makefile для сборки proxcluster коллектора
- Исправлены типы данных в main.go файлах коллекторов
This commit is contained in:
Sergey Antropoff 2025-09-12 00:11:32 +03:00
parent c3a81d963f
commit 8b8f26909c
19 changed files with 1849 additions and 2779 deletions

View File

@ -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

View File

@ -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]

1826
prox.json

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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)

View File

@ -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{}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View 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
}

View 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...)
}
}

View File

@ -0,0 +1,12 @@
// +build !linux
package main
import (
"os"
)
func main() {
// Платформа не поддерживается
os.Exit(1)
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
} }

View File

@ -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():