Files
ProxmoxDash/system_health.go

117 lines
3.2 KiB
Go

package main
import (
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
type SystemIssue struct {
Time int64 `json:"time"`
Unit string `json:"unit"`
Priority string `json:"priority"`
Message string `json:"message"`
Count int `json:"count"`
}
type SystemHealthMetrics struct {
Available bool `json:"available"`
FailedUnits int `json:"failedUnits"`
KernelErrors int `json:"kernelErrors"`
SystemErrors int `json:"systemErrors"`
Issues []SystemIssue `json:"issues"`
FailedServices []FailedService `json:"failedServices"`
CheckedAt int64 `json:"checkedAt"`
}
type FailedService struct {
Unit string `json:"unit"`
Load string `json:"load"`
Active string `json:"active"`
State string `json:"state"`
Description string `json:"description"`
}
type systemHealthCollector struct {
mu sync.RWMutex
value SystemHealthMetrics
}
func newSystemHealthCollector() *systemHealthCollector {
c := &systemHealthCollector{}
go c.run()
return c
}
func (c *systemHealthCollector) collect() SystemHealthMetrics {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
func (c *systemHealthCollector) run() {
c.refresh()
t := time.NewTicker(time.Minute)
defer t.Stop()
for range t.C {
c.refresh()
}
}
func (c *systemHealthCollector) refresh() {
m := SystemHealthMetrics{CheckedAt: time.Now().Unix()}
if _, err := exec.LookPath("journalctl"); err != nil {
c.mu.Lock()
c.value = m
c.mu.Unlock()
return
}
m.Available = true
if out, err := exec.Command("systemctl", "--failed", "--no-legend", "--plain").Output(); err == nil {
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
unit := strings.TrimPrefix(fields[0], "●")
offset := 1
if unit == "" && len(fields) > 4 {
unit = fields[1]
offset = 2
}
description := ""
if len(fields) > offset+3 {
description = strings.Join(fields[offset+3:], " ")
}
m.FailedServices = append(m.FailedServices, FailedService{Unit: unit, Load: fields[offset], Active: fields[offset+1], State: fields[offset+2], Description: description})
m.FailedUnits++
}
}
out, _ := exec.Command("journalctl", "--since", "24 hours ago", "-p", "0..3", "--no-pager", "-o", "short-unix", "-n", "100").Output()
pidPattern := regexp.MustCompile(`\[[0-9]+\]`)
grouped := map[string]int{}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
f := strings.Fields(line)
if len(f) < 2 {
continue
}
raw := strings.TrimSuffix(f[0], ":")
sec, _ := strconv.ParseFloat(raw, 64)
unit := strings.TrimSuffix(f[1], ":")
message := strings.Join(f[2:], " ")
normalized := pidPattern.ReplaceAllString(message, "[*]")
key := unit + "\x00" + normalized
if index, ok := grouped[key]; ok {
m.Issues[index].Count++
m.Issues[index].Time = int64(sec)
} else {
grouped[key] = len(m.Issues)
m.Issues = append(m.Issues, SystemIssue{Time: int64(sec), Unit: unit, Priority: "error", Message: normalized, Count: 1})
}
m.SystemErrors++
if strings.Contains(strings.ToLower(unit), "kernel") {
m.KernelErrors++
}
}
c.mu.Lock()
c.value = m
c.mu.Unlock()
}