forgejo-runner-optimiser/internal/proc/cgroup.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

59 lines
1.4 KiB
Go

// ABOUTME: Reads cgroup information from /proc/[pid]/cgroup.
// ABOUTME: Supports both cgroup v1 and v2 formats.
package proc
import (
"bufio"
"fmt"
"os"
"strings"
)
// CgroupInfo holds cgroup information for a process
type CgroupInfo struct {
Path string // The cgroup path (unified for v2, or from memory controller for v1)
}
// ReadCgroup reads /proc/[pid]/cgroup and extracts the cgroup path
func ReadCgroup(procPath string, pid int) (*CgroupInfo, error) {
path := fmt.Sprintf("%s/%d/cgroup", procPath, pid)
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening cgroup file: %w", err)
}
defer func() { _ = file.Close() }()
var cgroupPath string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// Try cgroup v2 first (unified hierarchy)
// Format: 0::/path
if path, found := strings.CutPrefix(line, "0::"); found {
cgroupPath = path
break
}
// Fall back to cgroup v1 - look for memory controller
// Format: X:memory:/path or X:memory,other:/path
parts := strings.SplitN(line, ":", 3)
if len(parts) == 3 {
controllers := parts[1]
if strings.Contains(controllers, "memory") {
cgroupPath = parts[2]
// Don't break - prefer v2 if found later
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scanning cgroup file: %w", err)
}
return &CgroupInfo{
Path: cgroupPath,
}, nil
}