forgejo-runner-optimiser/internal/output/logger.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

154 lines
3.6 KiB
Go

package output
import (
"context"
"io"
"log/slog"
"os"
"edp.buildth.ing/DevFW-CICD/forgejo-runner-resource-collector/internal/metrics"
)
// LogFormat specifies the log output format
type LogFormat string
const (
LogFormatJSON LogFormat = "json"
LogFormatText LogFormat = "text"
)
// LoggerWriter outputs metrics using structured logging
type LoggerWriter struct {
logger *slog.Logger
level slog.Level
}
// LoggerConfig holds configuration for the logger
type LoggerConfig struct {
Output io.Writer
Format LogFormat
Level slog.Level
}
// NewLoggerWriter creates a new logger-based writer
func NewLoggerWriter(cfg LoggerConfig) *LoggerWriter {
if cfg.Output == nil {
cfg.Output = os.Stdout
}
var handler slog.Handler
opts := &slog.HandlerOptions{
Level: cfg.Level,
}
switch cfg.Format {
case LogFormatText:
handler = slog.NewTextHandler(cfg.Output, opts)
default:
handler = slog.NewJSONHandler(cfg.Output, opts)
}
return &LoggerWriter{
logger: slog.New(handler),
level: cfg.Level,
}
}
// topCPUEntry is a lightweight struct for JSON serialization of top CPU processes
type topCPUEntry struct {
PID int `json:"pid"`
Name string `json:"name"`
CPUPercent float64 `json:"cpu_percent"`
}
// topMemEntry is a lightweight struct for JSON serialization of top memory processes
type topMemEntry struct {
PID int `json:"pid"`
Name string `json:"name"`
RSSBytes uint64 `json:"rss_bytes"`
}
// Write outputs the metrics using structured logging
func (w *LoggerWriter) Write(m *metrics.SystemMetrics) error {
// Build top CPU process entries
topCPU := make([]topCPUEntry, 0, len(m.TopCPU))
for _, p := range m.TopCPU {
topCPU = append(topCPU, topCPUEntry{
PID: p.PID,
Name: p.Name,
CPUPercent: p.CPUPercent,
})
}
// Build top memory process entries
topMem := make([]topMemEntry, 0, len(m.TopMemory))
for _, p := range m.TopMemory {
topMem = append(topMem, topMemEntry{
PID: p.PID,
Name: p.Name,
RSSBytes: p.MemRSS,
})
}
// Build base attributes
attrs := []slog.Attr{
slog.Time("collection_time", m.Timestamp),
slog.Int("total_processes", m.TotalProcesses),
slog.Group("cpu",
slog.Float64("total_percent", m.CPU.TotalPercent),
slog.Float64("user_percent", m.CPU.UserPercent),
slog.Float64("system_percent", m.CPU.SystemPercent),
slog.Float64("idle_percent", m.CPU.IdlePercent),
slog.Float64("iowait_percent", m.CPU.IOWaitPercent),
),
slog.Group("memory",
slog.Uint64("total_bytes", m.Memory.TotalBytes),
slog.Uint64("used_bytes", m.Memory.UsedBytes),
slog.Uint64("free_bytes", m.Memory.FreeBytes),
slog.Uint64("available_bytes", m.Memory.AvailableBytes),
slog.Float64("used_percent", m.Memory.UsedPercent),
slog.Uint64("total_rss_bytes", m.Memory.TotalRSSBytes),
slog.Float64("rss_percent", m.Memory.RSSPercent),
),
slog.Any("top_cpu", topCPU),
slog.Any("top_memory", topMem),
}
// Add cgroups if present
if len(m.Cgroups) > 0 {
attrs = append(attrs, slog.Any("cgroups", m.Cgroups))
}
w.logger.LogAttrs(context.Background(), slog.LevelInfo, "metrics_collected", attrs...)
return nil
}
// Close is a no-op for the logger writer
func (w *LoggerWriter) Close() error {
return nil
}
// ParseLogLevel parses a log level string
func ParseLogLevel(level string) slog.Level {
switch level {
case "debug":
return slog.LevelDebug
case "warn", "warning":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
// ParseLogFormat parses a log format string
func ParseLogFormat(format string) LogFormat {
switch format {
case "text":
return LogFormatText
default:
return LogFormatJSON
}
}