208 lines
4.5 KiB
Go
208 lines
4.5 KiB
Go
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)
|
|
}
|