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 }