host: cgroup and /proc readers for the metrics sampler
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+106
−0
internal/host/metrics_probes.go+106 −0
diff --git a/internal/host/metrics_probes.go b/internal/host/metrics_probes.go| new file mode 100644 |
| index 000000000..eb31b486e |
| --- /dev/null |
| +++ b/internal/host/metrics_probes.go |
| @@ -0,0 +1,106 @@ |
| +// The box-side half of the metrics sampler: every function here reads local |
| +// kernel/OS state (cgroup v2 and /proc files) and MUST fail toward nil, never |
| +// a guessed zero. This file is thin OS glue — os.ReadFile and os.ReadDir |
| +// wrapping the pure parsers in internal/metrics — and is deliberately untested |
| +// by the same convention as probes.go readers; internal/metrics tests exercise |
| +// the parsers with fixtures instead. |
| +package host |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/metrics" |
| +) |
| + |
| +// readProcStat reads /proc/stat's aggregate cpu line and parses its counters. |
| +// Read failure or parse failure -> nil. |
| +func readProcStat() *metrics.CPUCounters { |
| + b, err := os.ReadFile("/proc/stat") |
| + if err != nil { |
| + return nil |
| + } |
| + return metrics.ParseProcStat(string(b)) |
| +} |
| + |
| +// readLoadAvg reads /proc/loadavg and parses the 1/5/15-minute load averages. |
| +// Any read or parse failure (too few fields, non-numeric values) -> all three |
| +// results nil, since a partial load average is not a value worth publishing |
| +// on its own. |
| +func readLoadAvg() (l1, l5, l15 *float64) { |
| + b, err := os.ReadFile("/proc/loadavg") |
| + if err != nil { |
| + return nil, nil, nil |
| + } |
| + return metrics.ParseLoadAvg(string(b)) |
| +} |
| + |
| +// cgroupSliceDir is the cgroup v2 directory where CARLOS units report their |
| +// resource usage. |
| +const cgroupSliceDir = "/sys/fs/cgroup/system.slice" |
| + |
| +// listCarlosUnits lists all active CARLOS systemd units by reading the cgroup |
| +// slice directory and filtering for names with the "carlos-" prefix and |
| +// ".service" suffix. Read failure -> nil. |
| +func listCarlosUnits() []string { |
| + entries, err := os.ReadDir(cgroupSliceDir) |
| + if err != nil { |
| + return nil |
| + } |
| + |
| + var units []string |
| + for _, entry := range entries { |
| + name := entry.Name() |
| + // Match directories with prefix "carlos-" and suffix ".service" |
| + if entry.IsDir() && len(name) > len("carlos-.service") { |
| + if name[:7] == "carlos-" && name[len(name)-8:] == ".service" { |
| + units = append(units, name) |
| + } |
| + } |
| + } |
| + return units |
| +} |
| + |
| +// unitRaw is a single reading of a systemd unit's resource usage from its |
| +// cgroup v2 files. Each field represents a metric that may have failed to |
| +// read independently. |
| +type unitRaw struct { |
| + cpuUsec *int64 |
| + memBytes *int64 |
| + memPeakBytes *int64 |
| + readBytes *int64 |
| + writeBytes *int64 |
| +} |
| + |
| +// readUnitRaw reads a unit's resource usage from its cgroup v2 files under |
| +// cgroupSliceDir/<unit>/. Each file read failure sets only that field to nil; |
| +// the others still read independently. Parse failures also yield nil for that |
| +// field only. |
| +func readUnitRaw(unit string) unitRaw { |
| + unitPath := filepath.Join(cgroupSliceDir, unit) |
| + |
| + // Read cpu.stat for usage_usec |
| + raw := unitRaw{} |
| + if b, err := os.ReadFile(filepath.Join(unitPath, "cpu.stat")); err == nil { |
| + raw.cpuUsec = metrics.ParseCgroupCPUUsec(string(b)) |
| + } |
| + |
| + // Read memory.current |
| + if b, err := os.ReadFile(filepath.Join(unitPath, "memory.current")); err == nil { |
| + raw.memBytes = metrics.ParseCgroupInt(string(b)) |
| + } |
| + |
| + // Read memory.peak |
| + if b, err := os.ReadFile(filepath.Join(unitPath, "memory.peak")); err == nil { |
| + raw.memPeakBytes = metrics.ParseCgroupInt(string(b)) |
| + } |
| + |
| + // Read io.stat for rbytes and wbytes |
| + if b, err := os.ReadFile(filepath.Join(unitPath, "io.stat")); err == nil { |
| + rbytes, wbytes := metrics.ParseCgroupIO(string(b)) |
| + raw.readBytes = rbytes |
| + raw.writeBytes = wbytes |
| + } |
| + |
| + return raw |
| +} |