feat(collectors) basic implementation of boring collectors; sender via http(s) to hard coded api route
This commit is contained in:
9
internal/collectors/collector.go
Normal file
9
internal/collectors/collector.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package collectors
|
||||
|
||||
import "bugs.watch/agent/internal/aggregate"
|
||||
|
||||
// Collector collects one kind of system signal.
|
||||
type Collector interface {
|
||||
Collect(window *aggregate.Window)
|
||||
Name() string
|
||||
}
|
||||
105
internal/collectors/cpu/cpu.go
Normal file
105
internal/collectors/cpu/cpu.go
Normal 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
|
||||
}
|
||||
58
internal/collectors/disk/disk.go
Normal file
58
internal/collectors/disk/disk.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"bugs.watch/agent/internal/aggregate"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
TotalBytes uint64
|
||||
UsedBytes uint64
|
||||
}
|
||||
|
||||
// Read returns disk usage statistics for the root filesystem.
|
||||
func Read() (Stats, error) {
|
||||
var fs syscall.Statfs_t
|
||||
|
||||
if err := syscall.Statfs("/", &fs); err != nil {
|
||||
return Stats{}, err
|
||||
}
|
||||
|
||||
total := fs.Blocks * uint64(fs.Bsize)
|
||||
free := fs.Bavail * uint64(fs.Bsize)
|
||||
used := total - free
|
||||
|
||||
return Stats{
|
||||
TotalBytes: total,
|
||||
UsedBytes: used,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UsedPercent returns disk usage as a percentage.
|
||||
func UsedPercent(stats Stats) float64 {
|
||||
if stats.TotalBytes == 0 {
|
||||
return 0
|
||||
}
|
||||
return (float64(stats.UsedBytes) / float64(stats.TotalBytes)) * 100.0
|
||||
}
|
||||
|
||||
// Collector implements the collectors.Collector interface for disk usage.
|
||||
type Collector struct{}
|
||||
|
||||
func New() *Collector {
|
||||
return &Collector{}
|
||||
}
|
||||
|
||||
func (c *Collector) Name() string {
|
||||
return "disk"
|
||||
}
|
||||
|
||||
func (c *Collector) Collect(window *aggregate.Window) {
|
||||
stats, err := Read()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
window.AddDisk(UsedPercent(stats))
|
||||
}
|
||||
92
internal/collectors/memory/memory.go
Normal file
92
internal/collectors/memory/memory.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bugs.watch/agent/internal/aggregate"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
TotalKB uint64
|
||||
AvailableKB uint64
|
||||
}
|
||||
|
||||
// Read reads MemTotal and MemAvailable from /proc/meminfo.
|
||||
// Values are in KB.
|
||||
func Read() (Stats, error) {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
return Stats{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var total uint64
|
||||
var available uint64
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
if v, err := strconv.ParseUint(fields[1], 10, 64); err == nil {
|
||||
total = v
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "MemAvailable:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
if v, err := strconv.ParseUint(fields[1], 10, 64); err == nil {
|
||||
available = v
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return Stats{}, err
|
||||
}
|
||||
|
||||
return Stats{
|
||||
TotalKB: total,
|
||||
AvailableKB: available,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UsedPercent returns used memory percentage based on MemTotal and MemAvailable.
|
||||
func UsedPercent(s Stats) float64 {
|
||||
if s.TotalKB == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
used := s.TotalKB - s.AvailableKB
|
||||
return (float64(used) / float64(s.TotalKB)) * 100.0
|
||||
}
|
||||
|
||||
// Collector implements a stateless memory usage collector.
|
||||
type Collector struct{}
|
||||
|
||||
func New() *Collector {
|
||||
return &Collector{}
|
||||
}
|
||||
|
||||
func (c *Collector) Name() string {
|
||||
return "memory"
|
||||
}
|
||||
|
||||
func (c *Collector) Collect(window *aggregate.Window) {
|
||||
stats, err := Read()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
window.AddMemory(UsedPercent(stats))
|
||||
}
|
||||
41
internal/collectors/uptime/uptime.go
Normal file
41
internal/collectors/uptime/uptime.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package uptime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bugs.watch/agent/internal/aggregate"
|
||||
)
|
||||
|
||||
type Collector struct{}
|
||||
|
||||
func New() *Collector {
|
||||
return &Collector{}
|
||||
}
|
||||
|
||||
func (c *Collector) Name() string {
|
||||
return "uptime"
|
||||
}
|
||||
|
||||
// Collect reads /proc/uptime and stores the latest uptime in seconds.
|
||||
// Failure is silently ignored by design.
|
||||
func (c *Collector) Collect(window *aggregate.Window) {
|
||||
data, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Format: "<uptime_seconds> <idle_seconds>"
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
seconds, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
window.SetUptime(uint64(seconds))
|
||||
}
|
||||
Reference in New Issue
Block a user