package metrics import ( "bufio" "fmt" "os" "runtime" "strconv" "strings" "sync" "time" ) type Snapshot struct { CPUPercent float64 `json:"cpuPercent"` MemBytes int64 `json:"memBytes"` MemLimit int64 `json:"memLimit"` } type Collector struct { mu sync.Mutex lastCPUUseUse uint64 lastTime time.Time } func (c *Collector) Snapshot() Snapshot { c.mu.Lock() defer c.mu.Unlock() usage, okCPU := readCPUUsage() mem, limit, okMem := readMemoryUsage() cpuPercent := 0.0 if okCPU { now := time.Now() if !c.lastTime.IsZero() { deltaUsage := float64(usage - c.lastCPUUseUse) deltaTime := now.Sub(c.lastTime).Seconds() cpuQuota := cpuQuotaCount() if cpuQuota <= 0 { cpuQuota = float64(runtime.NumCPU()) } if deltaTime > 0 && cpuQuota > 0 { cpuPercent = (deltaUsage / (deltaTime * 1_000_000)) * (100 / cpuQuota) } } c.lastCPUUseUse = usage c.lastTime = now } if !okMem || mem <= 0 { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) mem = int64(memStats.Alloc) limit = int64(memStats.Sys) } return Snapshot{ CPUPercent: cpuPercent, MemBytes: mem, MemLimit: limit, } } func readCPUUsage() (uint64, bool) { file, err := os.Open("/sys/fs/cgroup/cpu.stat") if err != nil { return readCPUUsageV1() } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() parts := strings.Fields(line) if len(parts) == 2 && parts[0] == "usage_usec" { val, err := strconv.ParseUint(parts[1], 10, 64) if err == nil { return val, true } } } return readCPUUsageV1() } func readMemoryUsage() (int64, int64, bool) { current, err := os.ReadFile("/sys/fs/cgroup/memory.current") if err != nil { return readMemoryUsageV1() } limit, err := os.ReadFile("/sys/fs/cgroup/memory.max") if err != nil { return readMemoryUsageV1() } memBytes, err := strconv.ParseInt(strings.TrimSpace(string(current)), 10, 64) if err != nil { return 0, 0, false } limitRaw := strings.TrimSpace(string(limit)) if limitRaw == "max" { return memBytes, 0, true } limitBytes, err := strconv.ParseInt(limitRaw, 10, 64) if err != nil { return memBytes, 0, true } return memBytes, limitBytes, true } func readCPUUsageV1() (uint64, bool) { data, err := os.ReadFile("/sys/fs/cgroup/cpuacct/cpuacct.usage") if err != nil { return readProcCPUUsage() } val, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64) if err != nil { return readProcCPUUsage() } return val / 1000, true } func readMemoryUsageV1() (int64, int64, bool) { current, err := os.ReadFile("/sys/fs/cgroup/memory/memory.usage_in_bytes") if err != nil { return 0, 0, false } limit, err := os.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes") if err != nil { return 0, 0, false } memBytes, err := strconv.ParseInt(strings.TrimSpace(string(current)), 10, 64) if err != nil { return 0, 0, false } limitBytes, err := strconv.ParseInt(strings.TrimSpace(string(limit)), 10, 64) if err != nil { return memBytes, 0, true } return memBytes, limitBytes, true } func readProcCPUUsage() (uint64, bool) { data, err := os.ReadFile("/proc/self/stat") if err != nil { return 0, false } contents := string(data) closeIdx := strings.LastIndex(contents, ")") if closeIdx == -1 || closeIdx+2 >= len(contents) { return 0, false } fields := strings.Fields(contents[closeIdx+2:]) if len(fields) < 15 { return 0, false } utime, err := strconv.ParseUint(fields[11], 10, 64) if err != nil { return 0, false } stime, err := strconv.ParseUint(fields[12], 10, 64) if err != nil { return 0, false } clkTck := uint64(100) totalTicks := utime + stime usec := (totalTicks * 1_000_000) / clkTck return usec, true } func cpuQuotaCount() float64 { data, err := os.ReadFile("/sys/fs/cgroup/cpu.max") if err != nil { return float64(runtime.NumCPU()) } parts := strings.Fields(string(data)) if len(parts) != 2 { return 0 } if parts[0] == "max" { return float64(runtime.NumCPU()) } quota, err := strconv.ParseFloat(parts[0], 64) if err != nil { return 0 } period, err := strconv.ParseFloat(parts[1], 64) if err != nil || period == 0 { return 0 } return quota / period } func FormatBytes(size int64) string { if size < 1024 { return fmt.Sprintf("%d B", size) } kb := float64(size) / 1024 if kb < 1024 { return fmt.Sprintf("%.1f KB", kb) } mb := kb / 1024 if mb < 1024 { return fmt.Sprintf("%.1f MB", mb) } gb := mb / 1024 return fmt.Sprintf("%.2f GB", gb) }