feat(collectors) basic implementation of boring collectors; sender via http(s) to hard coded api route

This commit is contained in:
2026-01-18 19:21:16 +00:00
parent e64f597fab
commit db6ed003f1
12 changed files with 611 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
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
}