safesight/agent/internal/metrics/metrics.go
2026-01-17 16:57:51 +08:00

264 lines
5.5 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
}