carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package metrics is the pure half of host-process monitoring: every
2// function here parses a string or a pair of prior readings and MUST fail
3// toward nil, never a guessed zero. A zero CPU fraction and "we couldn't
4// read /proc/stat" are different facts, and a caller several layers up
5// (status pages, alerting) cannot tell them apart once a parser has already
6// collapsed one into the other — so a garbled line, a missing field, or a
7// counter that has gone backwards (reboot, cgroup recreated) all return nil
8// rather than 0 or the previous value. Nothing in this package touches the
9// filesystem or the OS: callers own reading /proc/stat, /proc/loadavg, and
10// cgroup v2 files, and pass the resulting text or prior/current readings in.
11package metrics
12
13import (
14 "strconv"
15 "strings"
16 "time"
17)
18
19// CPUCounters is a monotonic reading of /proc/stat's aggregate cpu line.
20type CPUCounters struct{ Busy, Total uint64 }
21
22// ParseProcStat parses the aggregate "cpu" line of /proc/stat (fields after
23// "cpu" are user nice system idle iowait irq softirq steal, with optional
24// guest/guest_nice columns already folded into user/nice by the kernel on
25// modern systems). Total is the sum of the first 8 fields; Busy is Total
26// minus idle and iowait — deliberately only the first 8, so a kernel that
27// adds guest columns is never double-counted. Any missing "cpu" prefix, too
28// few fields, or a non-numeric field -> nil.
29func ParseProcStat(text string) *CPUCounters {
30 for _, line := range strings.Split(text, "\n") {
31 fields := strings.Fields(line)
32 if len(fields) < 9 || fields[0] != "cpu" {
33 continue
34 }
35 var vals [8]uint64
36 ok := true
37 for i := 0; i < 8; i++ {
38 v, err := strconv.ParseUint(fields[i+1], 10, 64)
39 if err != nil {
40 ok = false
41 break
42 }
43 vals[i] = v
44 }
45 if !ok {
46 continue
47 }
48 var total uint64
49 for _, v := range vals {
50 total += v
51 }
52 idle, iowait := vals[3], vals[4]
53 return &CPUCounters{Busy: total - idle - iowait, Total: total}
54 }
55 return nil
56}
57
58// CPUFrac turns two /proc/stat readings into a fraction of CPU busy over the
59// interval between them. Both readings must be non-nil, cur.Total must
60// exceed prev.Total, and cur.Busy must not be behind prev.Busy — counters
61// that went backwards mean the box rebooted or the reading is otherwise
62// invalid, and reporting a fraction from that would be a guess dressed up as
63// a measurement, so it's nil instead. The Busy check matters on its own:
64// Total can still advance while Busy alone regresses, and since both are
65// unsigned, cur.Busy-prev.Busy would silently wrap into a huge garbage
66// value rather than go negative.
67func CPUFrac(prev, cur *CPUCounters) *float64 {
68 if prev == nil || cur == nil || cur.Total <= prev.Total || cur.Busy < prev.Busy {
69 return nil
70 }
71 frac := float64(cur.Busy-prev.Busy) / float64(cur.Total-prev.Total)
72 return &frac
73}
74
75// ParseLoadAvg parses /proc/loadavg's first three whitespace-separated
76// fields (1/5/15-minute load averages). Any missing or non-numeric field
77// fails the whole reading -> all three results nil, since a partial load
78// average (e.g. l1 present but l5 garbled) is not a value worth publishing
79// on its own.
80func ParseLoadAvg(text string) (l1, l5, l15 *float64) {
81 fields := strings.Fields(text)
82 if len(fields) < 3 {
83 return nil, nil, nil
84 }
85 v1, err1 := strconv.ParseFloat(fields[0], 64)
86 v5, err5 := strconv.ParseFloat(fields[1], 64)
87 v15, err15 := strconv.ParseFloat(fields[2], 64)
88 if err1 != nil || err5 != nil || err15 != nil {
89 return nil, nil, nil
90 }
91 return &v1, &v5, &v15
92}
93
94// ParseCgroupCPUUsec parses cpu.stat's usage_usec line. Missing key or a
95// non-numeric value -> nil.
96func ParseCgroupCPUUsec(text string) *int64 {
97 for _, line := range strings.Split(text, "\n") {
98 fields := strings.Fields(line)
99 if len(fields) != 2 || fields[0] != "usage_usec" {
100 continue
101 }
102 v, err := strconv.ParseInt(fields[1], 10, 64)
103 if err != nil {
104 return nil
105 }
106 return &v
107 }
108 return nil
109}
110
111// ParseCgroupInt parses a single cgroup v2 scalar file such as
112// memory.current or memory.peak. cgroup v2 uses the literal string "max" to
113// mean "no limit set" — a real value, but not a number, so it maps to nil
114// exactly like a parse failure would, since callers only want a byte count
115// or nothing.
116func ParseCgroupInt(text string) *int64 {
117 s := strings.TrimSpace(text)
118 if s == "" || s == "max" {
119 return nil
120 }
121 v, err := strconv.ParseInt(s, 10, 64)
122 if err != nil {
123 return nil
124 }
125 return &v
126}
127
128// ParseCgroupIO parses io.stat, summing rbytes and wbytes across every
129// per-device line (a cgroup can have I/O against several block devices at
130// once, and the caller wants one whole-cgroup total). A line missing both
131// keys is simply skipped rather than failing the whole parse — io.stat's
132// per-device format is otherwise stable — but if no line anywhere yields
133// either key, both results are nil rather than a false zero.
134func ParseCgroupIO(text string) (rbytes, wbytes *int64) {
135 var rsum, wsum int64
136 found := false
137 for _, line := range strings.Split(text, "\n") {
138 for _, field := range strings.Fields(line) {
139 key, val, ok := strings.Cut(field, "=")
140 if !ok {
141 continue
142 }
143 switch key {
144 case "rbytes":
145 if v, err := strconv.ParseInt(val, 10, 64); err == nil {
146 rsum += v
147 found = true
148 }
149 case "wbytes":
150 if v, err := strconv.ParseInt(val, 10, 64); err == nil {
151 wsum += v
152 found = true
153 }
154 }
155 }
156 }
157 if !found {
158 return nil, nil
159 }
160 return &rsum, &wsum
161}
162
163// UnitCPUFrac turns two cgroup cpu.stat usage_usec readings and the elapsed
164// wall time between them into a fraction of a CPU core busy. Both readings
165// must be non-nil, cur must not be behind prev (cgroup recreated, counter
166// reset), and elapsed must be strictly positive — any of those failing
167// would turn a division into either a guess or a crash, so it's nil instead.
168func UnitCPUFrac(prevUsec, curUsec *int64, elapsed time.Duration) *float64 {
169 if prevUsec == nil || curUsec == nil || *curUsec < *prevUsec || elapsed <= 0 {
170 return nil
171 }
172 frac := float64(*curUsec-*prevUsec) / 1e6 / elapsed.Seconds()
173 return &frac
174}
175