Public source validation / validate (push) Failing after 3m8s
181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
package hostcollect
|
|
|
|
import (
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/itworx/pulse/internal/host"
|
|
)
|
|
|
|
// cpuTimes holds the three aggregates a utilisation delta needs, all in USER_HZ ticks.
|
|
type cpuTimes struct {
|
|
total uint64
|
|
idle uint64
|
|
iowait uint64
|
|
}
|
|
|
|
type cpuSample struct {
|
|
at time.Time
|
|
all cpuTimes
|
|
cores []cpuTimes
|
|
valid bool
|
|
}
|
|
|
|
// parseProcStat reads the cpu aggregate and per-core lines of /proc/stat.
|
|
//
|
|
// Column layout (proc(5)): user nice system idle iowait irq softirq steal guest
|
|
// guest_nice. guest and guest_nice are deliberately excluded from the total: the
|
|
// kernel already counts guest time inside user and nice, so including them again
|
|
// inflates the denominator and understates utilisation. Old kernels publish fewer
|
|
// columns, so anything past idle is optional.
|
|
func parseProcStat(data []byte) (cpuSample, error) {
|
|
sample := cpuSample{}
|
|
type indexedCore struct {
|
|
index int
|
|
times cpuTimes
|
|
}
|
|
cores := make([]indexedCore, 0, 8)
|
|
for _, line := range lines(data) {
|
|
if !strings.HasPrefix(line, "cpu") {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 5 {
|
|
continue
|
|
}
|
|
times, ok := parseCPUTimes(fields[1:])
|
|
if !ok {
|
|
continue
|
|
}
|
|
if fields[0] == "cpu" {
|
|
sample.all = times
|
|
sample.valid = true
|
|
continue
|
|
}
|
|
index, err := strconv.Atoi(strings.TrimPrefix(fields[0], "cpu"))
|
|
if err != nil || index < 0 {
|
|
continue
|
|
}
|
|
cores = append(cores, indexedCore{index: index, times: times})
|
|
}
|
|
if !sample.valid {
|
|
return cpuSample{}, errNoCPUAggregate
|
|
}
|
|
sort.Slice(cores, func(i, j int) bool { return cores[i].index < cores[j].index })
|
|
sample.cores = make([]cpuTimes, 0, len(cores))
|
|
for _, core := range cores {
|
|
sample.cores = append(sample.cores, core.times)
|
|
}
|
|
return sample, nil
|
|
}
|
|
|
|
// parseCPUTimes sums the first eight jiffy columns; a non-numeric column makes the
|
|
// whole line untrustworthy.
|
|
func parseCPUTimes(fields []string) (cpuTimes, bool) {
|
|
times := cpuTimes{}
|
|
limit := len(fields)
|
|
if limit > 8 {
|
|
limit = 8
|
|
}
|
|
for index := 0; index < limit; index++ {
|
|
value, ok := parseUint(fields[index])
|
|
if !ok {
|
|
return cpuTimes{}, false
|
|
}
|
|
times.total += value
|
|
switch index {
|
|
case 3:
|
|
times.idle = value
|
|
case 4:
|
|
times.iowait = value
|
|
}
|
|
}
|
|
return times, true
|
|
}
|
|
|
|
// cpuUsage samples /proc/stat and reports utilisation relative to the previous sample.
|
|
// The first call after start (or after a counter reset) reports no percentages at all
|
|
// rather than a fabricated value: an absolute counter carries no utilisation.
|
|
func (c *Collector) cpuUsage(now time.Time, warnings *warningSet) host.RawCPU {
|
|
data, err := readLimited(c.procPath("stat"), maxFileBytes)
|
|
if err != nil {
|
|
warnings.add(WarningCPUUnavailable)
|
|
return host.RawCPU{}
|
|
}
|
|
current, err := parseProcStat(data)
|
|
if err != nil {
|
|
warnings.add(WarningCPUUnavailable)
|
|
return host.RawCPU{}
|
|
}
|
|
current.at = now
|
|
|
|
c.mu.Lock()
|
|
previous := c.cpu
|
|
c.cpu = ¤t
|
|
c.mu.Unlock()
|
|
|
|
if previous == nil || !previous.valid {
|
|
warnings.add(WarningCPUFirstSample)
|
|
return host.RawCPU{}
|
|
}
|
|
busy, iowait, ok := utilisation(previous.all, current.all)
|
|
if !ok {
|
|
warnings.add(WarningCPUCounterReset)
|
|
return host.RawCPU{}
|
|
}
|
|
result := host.RawCPU{TotalPercent: &busy, IOWaitPercent: &iowait}
|
|
if len(previous.cores) != len(current.cores) {
|
|
warnings.add(WarningCPUTopologyChanged)
|
|
return result
|
|
}
|
|
cores := make([]float64, 0, len(current.cores))
|
|
for index := range current.cores {
|
|
corePercent, _, coreOK := utilisation(previous.cores[index], current.cores[index])
|
|
if !coreOK {
|
|
warnings.add(WarningCPUCounterReset)
|
|
return result
|
|
}
|
|
cores = append(cores, corePercent)
|
|
}
|
|
if len(cores) > c.options.HostLimits.MaxCores {
|
|
cores = cores[:c.options.HostLimits.MaxCores]
|
|
warnings.add(WarningCPUCoresTruncated)
|
|
}
|
|
result.PerCore = cores
|
|
return result
|
|
}
|
|
|
|
// utilisation converts two counter readings into busy and iowait percentages. Any
|
|
// counter going backwards means the counter wrapped or the source was replaced (a
|
|
// container restart, a CPU hot-unplug, a fixture root swap); the delta is then
|
|
// meaningless and is reported as unusable instead of as a huge or negative spike.
|
|
func utilisation(previous, current cpuTimes) (busyPercent, iowaitPercent float64, ok bool) {
|
|
if current.total < previous.total || current.idle < previous.idle || current.iowait < previous.iowait {
|
|
return 0, 0, false
|
|
}
|
|
totalDelta := current.total - previous.total
|
|
if totalDelta == 0 {
|
|
return 0, 0, false
|
|
}
|
|
idleDelta := current.idle - previous.idle
|
|
iowaitDelta := current.iowait - previous.iowait
|
|
if idleDelta+iowaitDelta > totalDelta {
|
|
return 0, 0, false
|
|
}
|
|
busy := float64(totalDelta-idleDelta-iowaitDelta) / float64(totalDelta) * 100
|
|
iowait := float64(iowaitDelta) / float64(totalDelta) * 100
|
|
return clampPercent(busy), clampPercent(iowait), true
|
|
}
|
|
|
|
func clampPercent(value float64) float64 {
|
|
if value < 0 {
|
|
return 0
|
|
}
|
|
if value > 100 {
|
|
return 100
|
|
}
|
|
return value
|
|
}
|