first
This commit is contained in:
207
backend/internal/metrics/metrics.go
Normal file
207
backend/internal/metrics/metrics.go
Normal file
@@ -0,0 +1,207 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user