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() }