package cpu import ( "bufio" "os" "strconv" "strings" "bugs.watch/agent/internal/aggregate" ) type Sample struct { Idle uint64 Total uint64 } // ReadSample reads CPU counters from /proc/stat. // It returns cumulative counters since boot. func ReadSample() (Sample, error) { f, err := os.Open("/proc/stat") if err != nil { return Sample{}, err } defer f.Close() scanner := bufio.NewScanner(f) if !scanner.Scan() { if err := scanner.Err(); err != nil { return Sample{}, err } return Sample{}, strconv.ErrSyntax } // First line: // cpu user nice system idle iowait irq softirq steal guest guest_nice fields := strings.Fields(scanner.Text()) if len(fields) < 5 || fields[0] != "cpu" { return Sample{}, strconv.ErrSyntax } var total uint64 for i := 1; i < len(fields); i++ { v, err := strconv.ParseUint(fields[i], 10, 64) if err != nil { return Sample{}, err } total += v } idle, err := strconv.ParseUint(fields[4], 10, 64) if err != nil { return Sample{}, err } return Sample{Idle: idle, Total: total}, nil } // UsagePercent computes CPU usage percentage between two samples. func UsagePercent(prev, curr Sample) float64 { // Guard against counter resets or weirdness if curr.Total <= prev.Total || curr.Idle < prev.Idle { return 0 } idleDelta := curr.Idle - prev.Idle totalDelta := curr.Total - prev.Total if totalDelta == 0 { return 0 } usedDelta := totalDelta - idleDelta return (float64(usedDelta) / float64(totalDelta)) * 100.0 } // Collector implements a stateful CPU usage collector. // It stores the previous sample internally so the agent does not have to. type Collector struct { prev Sample hasPrev bool } func New() *Collector { return &Collector{} } func (c *Collector) Name() string { return "cpu" } // Collect reads current CPU counters and, if possible, emits a usage percent. // On first run it will not emit (no previous sample yet). func (c *Collector) Collect(window *aggregate.Window) { curr, err := ReadSample() if err != nil { return } if c.hasPrev { usage := UsagePercent(c.prev, curr) window.AddCPU(usage) } c.prev = curr c.hasPrev = true }