117 lines
3.2 KiB
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()
|
|
}
|