carlos / platform Public

host: cgroup and /proc readers for the metrics sampler

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 6a997756563c2478c73282a7d286de8ca1bbf9f7 parent 7167ba8
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
+}