215 lines
4.6 KiB
Go
215 lines
4.6 KiB
Go
package metrics
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// SystemMetrics holds current system resource usage.
|
|
type SystemMetrics struct {
|
|
CPUPercent float64
|
|
MemoryPercent float64
|
|
DiskPercent float64
|
|
NetworkRxBytes float64
|
|
NetworkTxBytes float64
|
|
}
|
|
|
|
// Collector collects system metrics from /proc and sysfs.
|
|
type Collector struct {
|
|
lastCPUTotal uint64
|
|
lastCPUIdle uint64
|
|
lastNetRx float64
|
|
lastNetTx float64
|
|
lastNetTime time.Time
|
|
}
|
|
|
|
// NewCollector creates a new metrics collector.
|
|
func NewCollector() *Collector {
|
|
return &Collector{}
|
|
}
|
|
|
|
// Collect gathers current system metrics.
|
|
func (c *Collector) Collect() (SystemMetrics, error) {
|
|
var m SystemMetrics
|
|
|
|
cpu, err := c.readCPU()
|
|
if err == nil {
|
|
m.CPUPercent = cpu
|
|
}
|
|
|
|
mem, err := c.readMemory()
|
|
if err == nil {
|
|
m.MemoryPercent = mem
|
|
}
|
|
|
|
disk, err := c.readDisk("/")
|
|
if err == nil {
|
|
m.DiskPercent = disk
|
|
}
|
|
|
|
netRx, netTx, err := c.readNetwork()
|
|
if err == nil {
|
|
m.NetworkRxBytes = netRx
|
|
m.NetworkTxBytes = netTx
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// readCPU returns CPU usage percentage since last call.
|
|
func (c *Collector) readCPU() (float64, error) {
|
|
f, err := os.Open("/proc/stat")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if !strings.HasPrefix(line, "cpu ") {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 8 {
|
|
return 0, nil
|
|
}
|
|
|
|
var user, nice, system, idle, iowait, irq, softirq uint64
|
|
user, _ = strconv.ParseUint(fields[1], 10, 64)
|
|
nice, _ = strconv.ParseUint(fields[2], 10, 64)
|
|
system, _ = strconv.ParseUint(fields[3], 10, 64)
|
|
idle, _ = strconv.ParseUint(fields[4], 10, 64)
|
|
iowait, _ = strconv.ParseUint(fields[5], 10, 64)
|
|
irq, _ = strconv.ParseUint(fields[6], 10, 64)
|
|
softirq, _ = strconv.ParseUint(fields[7], 10, 64)
|
|
|
|
total := user + nice + system + idle + iowait + irq + softirq
|
|
idleTotal := idle + iowait
|
|
|
|
if c.lastCPUTotal > 0 {
|
|
totalDiff := total - c.lastCPUTotal
|
|
idleDiff := idleTotal - c.lastCPUIdle
|
|
|
|
if totalDiff > 0 {
|
|
cpuPercent := float64(totalDiff-idleDiff) / float64(totalDiff) * 100.0
|
|
c.lastCPUTotal = total
|
|
c.lastCPUIdle = idleTotal
|
|
return cpuPercent, nil
|
|
}
|
|
}
|
|
|
|
c.lastCPUTotal = total
|
|
c.lastCPUIdle = idleTotal
|
|
return 0, nil
|
|
}
|
|
|
|
return 0, scanner.Err()
|
|
}
|
|
|
|
// readMemory returns RAM usage percentage.
|
|
func (c *Collector) readMemory() (float64, error) {
|
|
f, err := os.Open("/proc/meminfo")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var total, available uint64
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.HasPrefix(line, "MemTotal:") {
|
|
fields := strings.Fields(line)
|
|
total, _ = strconv.ParseUint(fields[1], 10, 64)
|
|
} else if strings.HasPrefix(line, "MemAvailable:") {
|
|
fields := strings.Fields(line)
|
|
available, _ = strconv.ParseUint(fields[1], 10, 64)
|
|
}
|
|
}
|
|
|
|
if total == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
used := total - available
|
|
return float64(used) / float64(total) * 100.0, nil
|
|
}
|
|
|
|
// readDisk returns disk usage percentage for the given path.
|
|
func (c *Collector) readDisk(path string) (float64, error) {
|
|
var stat syscall.Statfs_t
|
|
if err := syscall.Statfs(path, &stat); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
total := stat.Blocks * uint64(stat.Bsize)
|
|
free := stat.Bfree * uint64(stat.Bsize)
|
|
|
|
if total == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
used := total - free
|
|
return float64(used) / float64(total) * 100.0, nil
|
|
}
|
|
|
|
// readNetwork returns network RX/TX bytes per second.
|
|
func (c *Collector) readNetwork() (float64, float64, error) {
|
|
f, err := os.Open("/proc/net/dev")
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var totalRx, totalTx uint64
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
// Skip header lines
|
|
if strings.Contains(line, "|") || strings.HasPrefix(strings.TrimSpace(line), "Inter") {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(strings.TrimSpace(line), ":", 2)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(parts[1])
|
|
if len(fields) < 9 {
|
|
continue
|
|
}
|
|
|
|
rx, _ := strconv.ParseUint(fields[0], 10, 64)
|
|
tx, _ := strconv.ParseUint(fields[8], 10, 64)
|
|
totalRx += rx
|
|
totalTx += tx
|
|
}
|
|
|
|
now := time.Now()
|
|
var rxRate, txRate float64
|
|
|
|
if !c.lastNetTime.IsZero() {
|
|
elapsed := now.Sub(c.lastNetTime).Seconds()
|
|
if elapsed > 0 {
|
|
rxRate = float64(totalRx) - c.lastNetRx
|
|
txRate = float64(totalTx) - c.lastNetTx
|
|
// Convert to bytes per second
|
|
rxRate = rxRate / elapsed
|
|
txRate = txRate / elapsed
|
|
}
|
|
}
|
|
|
|
c.lastNetRx = float64(totalRx)
|
|
c.lastNetTx = float64(totalTx)
|
|
c.lastNetTime = now
|
|
|
|
return rxRate, txRate, nil
|
|
}
|