341 lines
7.2 KiB
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
|
|
}
|