forgejo-runner-optimiser/internal/cgroup/parse.go
Manuel Ganter 5e470c33a5
All checks were successful
ci / build (push) Successful in 30s
feat(collector): group CPU and memory metrics by cgroup
Add cgroup-based process grouping to the resource collector. Processes are
grouped by their cgroup path, with container names resolved via configurable
process-to-container mapping.

New features:
- Read cgroup info from /proc/[pid]/cgroup (supports v1 and v2)
- Parse K8s resource notation (500m, 1Gi, etc.) for CPU/memory limits
- Group metrics by container using CGROUP_PROCESS_MAP env var
- Calculate usage percentages against limits from CGROUP_LIMITS env var
- Output cgroup metrics with CPU cores used, memory RSS, and percentages

Environment variables:
- CGROUP_PROCESS_MAP: Map process names to container names for discovery
- CGROUP_LIMITS: Define CPU/memory limits per container in K8s notation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 14:50:36 +01:00

96 lines
2.5 KiB
Go

// ABOUTME: Parses Kubernetes-style resource notation for CPU and memory.
// ABOUTME: CPU: "500m" = 0.5 cores, "2" = 2 cores.
// ABOUTME: Memory: "1Gi" = 1 GiB, "512Mi" = 512 MiB, "1G" = 1 GB.
package cgroup
import (
"fmt"
"strconv"
"strings"
)
// ParseCPU parses Kubernetes CPU notation to cores.
// Examples: "500m" => 0.5, "2" => 2.0, "100m" => 0.1, "2000m" => 2.0
func ParseCPU(value string) (float64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, fmt.Errorf("empty CPU value")
}
// Handle millicores suffix
if strings.HasSuffix(value, "m") {
millis, err := strconv.ParseFloat(strings.TrimSuffix(value, "m"), 64)
if err != nil {
return 0, fmt.Errorf("parsing millicores: %w", err)
}
return millis / 1000.0, nil
}
// Plain number means cores
cores, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, fmt.Errorf("parsing cores: %w", err)
}
return cores, nil
}
// ParseMemory parses Kubernetes memory notation to bytes.
// Supports:
// - Binary suffixes: Ki, Mi, Gi, Ti (powers of 1024)
// - Decimal suffixes: K, M, G, T (powers of 1000)
// - Plain numbers: bytes
func ParseMemory(value string) (uint64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, fmt.Errorf("empty memory value")
}
// Binary suffixes (powers of 1024)
binarySuffixes := map[string]uint64{
"Ki": 1024,
"Mi": 1024 * 1024,
"Gi": 1024 * 1024 * 1024,
"Ti": 1024 * 1024 * 1024 * 1024,
}
// Decimal suffixes (powers of 1000)
decimalSuffixes := map[string]uint64{
"K": 1000,
"M": 1000 * 1000,
"G": 1000 * 1000 * 1000,
"T": 1000 * 1000 * 1000 * 1000,
}
// Try binary suffixes first (2-char)
for suffix, multiplier := range binarySuffixes {
if strings.HasSuffix(value, suffix) {
numStr := strings.TrimSuffix(value, suffix)
num, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, fmt.Errorf("parsing memory value: %w", err)
}
return uint64(num * float64(multiplier)), nil
}
}
// Try decimal suffixes (1-char)
for suffix, multiplier := range decimalSuffixes {
if strings.HasSuffix(value, suffix) {
numStr := strings.TrimSuffix(value, suffix)
num, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, fmt.Errorf("parsing memory value: %w", err)
}
return uint64(num * float64(multiplier)), nil
}
}
// Plain number (bytes)
bytes, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return 0, fmt.Errorf("parsing bytes: %w", err)
}
return bytes, nil
}