// Package check parses the health data fleet collects (docker ps, // df, systemctl --failed) into stable rows and findings an agent can // diff between runs. package check import ( "fmt" "strconv" "strings" ) // Container is one row of docker ps output. type Container struct { Name string State string // running, exited, restarting, ... Status string // "Up 2 hours (healthy)", "Exited (1) 3 days ago" Image string } // Healthy reports whether the container looks fine: running, and not // flagged unhealthy. func (c Container) Healthy() bool { return c.State == "running" && !strings.Contains(c.Status, "unhealthy") } // ParseDockerPS parses `docker ps -a --format // "{{.Names}}\t{{.State}}\t{{.Status}}\t{{.Image}}"`. CI job // containers (Gitea Actions workers) are transient and skipped. func ParseDockerPS(out string) []Container { var rows []Container for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.Split(line, "\t") if len(parts) < 4 { continue } if strings.HasPrefix(parts[0], "GITEA-ACTIONS-TASK-") { continue } rows = append(rows, Container{Name: parts[0], State: parts[1], Status: parts[2], Image: parts[3]}) } return rows } // Mount is one row of df output. type Mount struct { Target string UsedPct int Avail string } // ParseDF parses `df -h --output=target,pcent,avail`. func ParseDF(out string) []Mount { var rows []Mount for i, line := range strings.Split(out, "\n") { fields := strings.Fields(line) if i == 0 || len(fields) < 3 { // header continue } pct, err := strconv.Atoi(strings.TrimSuffix(fields[1], "%")) if err != nil { continue } rows = append(rows, Mount{Target: fields[0], UsedPct: pct, Avail: fields[2]}) } return rows } // ParseFailedUnits parses `systemctl --failed --no-legend --plain` // into unit names. func ParseFailedUnits(out string) []string { var units []string for _, line := range strings.Split(out, "\n") { fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "● ")) if len(fields) == 0 { continue } u := fields[0] if strings.Contains(u, ".") { units = append(units, u) } } return units } // Finding is one thing worth attention. type Finding struct { Host string Text string } func (f Finding) String() string { return f.Host + ": " + f.Text } // Evaluate turns parsed data into findings. diskWarnPct is the fill // grade that counts as a problem. func Evaluate(host string, containers []Container, mounts []Mount, failedSys, failedUser []string, diskWarnPct int) []Finding { var out []Finding for _, c := range containers { if !c.Healthy() { out = append(out, Finding{host, fmt.Sprintf("container %s: %s (%s)", c.Name, c.State, c.Status)}) } } for _, m := range mounts { if m.UsedPct >= diskWarnPct { out = append(out, Finding{host, fmt.Sprintf("disk %s at %d%% (%s left)", m.Target, m.UsedPct, m.Avail)}) } } for _, u := range failedSys { out = append(out, Finding{host, "failed unit: " + u}) } for _, u := range failedUser { out = append(out, Finding{host, "failed user unit: " + u}) } return out }