Files
mqtt_explorer/backend/internal/metrics/metrics.go
Gilles Soulier 383ad292d3 first
2025-12-24 14:47:39 +01:00

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)
}