Files
2026-10-27 16:57:53 -06:00

130 lines
3.2 KiB
Go

package beach
import (
"fmt"
"strconv"
"strings"
)
// Host is the machine Beach runs on.
type Host struct {
Hostname string `json:"hostname"`
Uptime float64 `json:"uptimeSeconds"`
Load [3]float64 `json:"load"`
CPUPct float64 `json:"cpuPct"`
CPUs int `json:"cpus"`
MemTotal uint64 `json:"memTotal"`
MemUsed uint64 `json:"memUsed"`
SwapTotal uint64 `json:"swapTotal"`
SwapUsed uint64 `json:"swapUsed"`
Disks []Filesystem `json:"filesystems"`
Supported bool `json:"supported"`
}
type Filesystem struct {
Mount string `json:"mount"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
}
// cpuTimes is the aggregate line of /proc/stat.
type cpuTimes struct{ busy, total uint64 }
func parseCPU(stat string) (cpuTimes, error) {
for _, line := range strings.Split(stat, "\n") {
f := strings.Fields(line)
if len(f) < 5 || f[0] != "cpu" {
continue
}
var v []uint64
for _, s := range f[1:] {
n, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return cpuTimes{}, err
}
v = append(v, n)
}
var total uint64
for i, n := range v {
if i < 8 { // guest times are already counted in user/nice
total += n
}
}
idle := v[3]
if len(v) > 4 {
idle += v[4] // iowait
}
return cpuTimes{busy: total - idle, total: total}, nil
}
return cpuTimes{}, fmt.Errorf("no cpu line")
}
// cpuPct is the busy share between two samples.
func cpuPct(a, b cpuTimes) float64 {
if b.total <= a.total || b.busy < a.busy {
return 0
}
return float64(b.busy-a.busy) / float64(b.total-a.total) * 100
}
func parseMeminfo(s string) (total, used, swapTotal, swapUsed uint64) {
kb := map[string]uint64{}
for _, line := range strings.Split(s, "\n") {
f := strings.Fields(line)
if len(f) >= 2 {
n, _ := strconv.ParseUint(f[1], 10, 64)
kb[strings.TrimSuffix(f[0], ":")] = n * 1024
}
}
total = kb["MemTotal"]
avail, ok := kb["MemAvailable"]
if !ok {
avail = kb["MemFree"] + kb["Buffers"] + kb["Cached"]
}
if avail <= total {
used = total - avail
}
swapTotal = kb["SwapTotal"]
if kb["SwapFree"] <= swapTotal {
swapUsed = swapTotal - kb["SwapFree"]
}
return
}
func parseLoad(s string) (l [3]float64) {
f := strings.Fields(s)
for i := 0; i < 3 && i < len(f); i++ {
l[i], _ = strconv.ParseFloat(f[i], 64)
}
return
}
func parseUptime(s string) float64 {
f := strings.Fields(s)
if len(f) == 0 {
return 0
}
v, _ := strconv.ParseFloat(f[0], 64)
return v
}
// realMounts picks the mounts worth showing from /proc/mounts: real block or
// ZFS filesystems, once per device, without Docker's own overlay mounts.
func realMounts(mounts string) []string {
ok := map[string]bool{"ext4": true, "xfs": true, "btrfs": true, "zfs": true, "ext3": true, "f2fs": true, "vfat": true, "ntfs": true, "ntfs3": true, "exfat": true}
seen := map[string]bool{}
var out []string
for _, line := range strings.Split(mounts, "\n") {
f := strings.Fields(line)
if len(f) < 3 || !ok[f[2]] || seen[f[0]] {
continue
}
if strings.HasPrefix(f[1], "/var/lib/docker") || strings.HasPrefix(f[1], "/proc") || strings.HasPrefix(f[1], "/sys") {
continue
}
seen[f[0]] = true
out = append(out, f[1])
}
return out
}