safesight-edge/agent/internal/metrics/metrics.go

341 lines
7.2 KiB
Go

package metrics
import (
"bufio"
"errors"
"os"
"strconv"
"strings"
)
var ErrNotSupported = errors.New("not supported")
type CPUStat struct {
Total uint64 `json:"total"`
Idle uint64 `json:"idle"`
}
type MemInfo struct {
TotalKB uint64 `json:"total_kb"`
AvailableKB uint64 `json:"available_kb"`
FreeKB uint64 `json:"free_kb"`
BuffersKB uint64 `json:"buffers_kb"`
CachedKB uint64 `json:"cached_kb"`
}
type NetIO struct {
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
}
type DiskIO struct {
ReadBytes uint64 `json:"read_bytes"`
WriteBytes uint64 `json:"write_bytes"`
}
type LoadAvg struct {
One float64 `json:"one"`
Five float64 `json:"five"`
Fifteen float64 `json:"fifteen"`
}
type DiskUsage struct {
Path string `json:"path"`
TotalBytes uint64 `json:"total_bytes"`
FreeBytes uint64 `json:"free_bytes"`
UsedBytes uint64 `json:"used_bytes"`
UsedPct float64 `json:"used_pct"`
}
func ReadCPUStat() (CPUStat, error) {
b, err := os.ReadFile("/proc/stat")
if err != nil {
return CPUStat{}, err
}
return ParseProcStat(string(b))
}
func ParseProcStat(data string) (CPUStat, error) {
s := bufio.NewScanner(strings.NewReader(data))
for s.Scan() {
line := strings.TrimSpace(s.Text())
if strings.HasPrefix(line, "cpu ") {
parts := strings.Fields(line)
if len(parts) < 5 {
return CPUStat{}, errors.New("invalid cpu stat")
}
var total uint64
var idle uint64
for i := 1; i < len(parts); i++ {
v, err := strconv.ParseUint(parts[i], 10, 64)
if err != nil {
return CPUStat{}, err
}
total += v
if i == 4 || i == 5 {
idle += v
}
}
return CPUStat{Total: total, Idle: idle}, nil
}
}
if err := s.Err(); err != nil {
return CPUStat{}, err
}
return CPUStat{}, errors.New("cpu line not found")
}
func CPUUsage(prev, cur CPUStat) float64 {
if cur.Total <= prev.Total {
return 0
}
totalDelta := cur.Total - prev.Total
idleDelta := uint64(0)
if cur.Idle > prev.Idle {
idleDelta = cur.Idle - prev.Idle
}
if totalDelta == 0 {
return 0
}
used := totalDelta - idleDelta
return float64(used) / float64(totalDelta) * 100
}
func ReadMemInfo() (MemInfo, error) {
b, err := os.ReadFile("/proc/meminfo")
if err != nil {
return MemInfo{}, err
}
return ParseMemInfo(string(b))
}
func ParseMemInfo(data string) (MemInfo, error) {
out := MemInfo{}
s := bufio.NewScanner(strings.NewReader(data))
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
key := strings.TrimSuffix(parts[0], ":")
val, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
continue
}
switch key {
case "MemTotal":
out.TotalKB = val
case "MemAvailable":
out.AvailableKB = val
case "MemFree":
out.FreeKB = val
case "Buffers":
out.BuffersKB = val
case "Cached":
out.CachedKB = val
}
}
if err := s.Err(); err != nil {
return MemInfo{}, err
}
if out.TotalKB == 0 {
return MemInfo{}, errors.New("meminfo missing MemTotal")
}
return out, nil
}
func ReadNetDev() (NetIO, error) {
b, err := os.ReadFile("/proc/net/dev")
if err != nil {
return NetIO{}, err
}
return ParseNetDev(string(b))
}
func ParseNetDev(data string) (NetIO, error) {
var out NetIO
s := bufio.NewScanner(strings.NewReader(data))
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, "Inter-") || strings.HasPrefix(line, "face") {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
name := strings.TrimSpace(parts[0])
if name == "lo" {
continue
}
fields := strings.Fields(parts[1])
if len(fields) < 9 {
continue
}
rx, err := strconv.ParseUint(fields[0], 10, 64)
if err != nil {
continue
}
tx, err := strconv.ParseUint(fields[8], 10, 64)
if err != nil {
continue
}
out.RxBytes += rx
out.TxBytes += tx
}
if err := s.Err(); err != nil {
return NetIO{}, err
}
return out, nil
}
func ReadDiskStats() (DiskIO, error) {
b, err := os.ReadFile("/proc/diskstats")
if err != nil {
return DiskIO{}, err
}
return ParseDiskStats(string(b))
}
func ParseDiskStats(data string) (DiskIO, error) {
var out DiskIO
s := bufio.NewScanner(strings.NewReader(data))
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 11 {
continue
}
name := fields[2]
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") {
continue
}
readSectors, err := strconv.ParseUint(fields[5], 10, 64)
if err != nil {
continue
}
writeSectors, err := strconv.ParseUint(fields[9], 10, 64)
if err != nil {
continue
}
out.ReadBytes += readSectors * 512
out.WriteBytes += writeSectors * 512
}
if err := s.Err(); err != nil {
return DiskIO{}, err
}
return out, nil
}
func ReadLoadAvg() (LoadAvg, error) {
b, err := os.ReadFile("/proc/loadavg")
if err != nil {
return LoadAvg{}, err
}
return ParseLoadAvg(string(b))
}
func ParseLoadAvg(data string) (LoadAvg, error) {
fields := strings.Fields(strings.TrimSpace(data))
if len(fields) < 3 {
return LoadAvg{}, errors.New("invalid loadavg")
}
one, err := strconv.ParseFloat(fields[0], 64)
if err != nil {
return LoadAvg{}, err
}
five, err := strconv.ParseFloat(fields[1], 64)
if err != nil {
return LoadAvg{}, err
}
fifteen, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
return LoadAvg{}, err
}
return LoadAvg{One: one, Five: five, Fifteen: fifteen}, nil
}
type NPULoad struct {
UsagePct float64 `json:"usage_pct"`
Cores map[string]float64 `json:"cores,omitempty"`
}
func ReadNPULoad() (*NPULoad, error) {
f, err := os.Open("/proc/rknpu/load")
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
cores := map[string]float64{}
n := 0
for scanner.Scan() {
line := scanner.Text()
// Find each "CoreN: XX%" pattern
idx := 0
for {
coreStart := strings.Index(line[idx:], "Core")
if coreStart < 0 {
break
}
coreStart += idx
colon := strings.Index(line[coreStart:], ":")
if colon < 0 {
break
}
colon += coreStart
coreName := strings.TrimSpace(line[coreStart:colon])
// Find value after colon up to % or comma or end
valEnd := strings.Index(line[colon+1:], "%")
if valEnd < 0 {
break
}
valEnd += colon + 1
valStr := strings.TrimSpace(line[colon+1 : valEnd])
v, err := strconv.ParseFloat(valStr, 64)
if err == nil {
cores[coreName] = v
n++
}
idx = valEnd + 1
}
}
if n == 0 {
return nil, ErrNotSupported
}
var total float64
for _, v := range cores {
total += v
}
avg := total / float64(n)
return &NPULoad{UsagePct: avg, Cores: cores}, nil
}
type Temperature struct {
Zone string `json:"zone"`
Celsius float64 `json:"celsius"`
}
func ReadTemperature() (*Temperature, error) {
raw, err := os.ReadFile("/sys/class/thermal/thermal_zone0/temp")
if err != nil {
return nil, err
}
zone, _ := os.ReadFile("/sys/class/thermal/thermal_zone0/type")
milli, err := strconv.ParseFloat(strings.TrimSpace(string(raw)), 64)
if err != nil {
return nil, err
}
return &Temperature{
Zone: strings.TrimSpace(string(zone)),
Celsius: milli / 1000,
}, nil
}