// Package collect reads system state from /proc, /sys and CLI tools. package collect import ( "bufio" "bytes" "fmt" "os" "runtime" "strconv" "strings" ) type CPU struct { UsagePct float64 `json:"usage_pct"` Cores int `json:"cores"` Load [3]float64 `json:"load"` } type cpuTimes struct{ idle, total uint64 } // CPUSampler keeps the previous /proc/stat reading to compute usage deltas. type CPUSampler struct { prev cpuTimes init bool } func (s *CPUSampler) Sample() (CPU, error) { b, err := os.ReadFile("/proc/stat") if err != nil { return CPU{}, err } cur, err := parseProcStat(b) if err != nil { return CPU{}, err } c := CPU{Cores: runtime.NumCPU()} if s.init && cur.total > s.prev.total { dt := float64(cur.total - s.prev.total) di := float64(cur.idle - s.prev.idle) c.UsagePct = 100 * (1 - di/dt) } s.prev, s.init = cur, true if lb, err := os.ReadFile("/proc/loadavg"); err == nil { f := strings.Fields(string(lb)) for i := 0; i < 3 && i < len(f); i++ { c.Load[i], _ = strconv.ParseFloat(f[i], 64) } } return c, nil } // parseProcStat returns idle and total jiffies from the aggregate "cpu" line. func parseProcStat(b []byte) (cpuTimes, error) { sc := bufio.NewScanner(bytes.NewReader(b)) for sc.Scan() { f := strings.Fields(sc.Text()) if len(f) < 5 || f[0] != "cpu" { continue } var t cpuTimes for i, v := range f[1:] { n, err := strconv.ParseUint(v, 10, 64) if err != nil { return t, err } t.total += n if i == 3 || i == 4 { // idle + iowait t.idle += n } } return t, nil } return cpuTimes{}, fmt.Errorf("no cpu line in /proc/stat") }