Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
How loaded a machine's storage is, read from `/proc` and `/sys` and nothing else: the share of CPU time
|
||||
spent waiting on I/O, I/O pressure stall, each disk's busy share, throughput, IOPS, wait and queue
|
||||
depth, the filesystems' space, and the units, containers and processes doing the I/O (novox/hq issue
|
||||
312). It changes nothing and declares no resources, so it can be assigned to every machine.
|
||||
312). It also reads a machine's CPU use by systemd unit and by container, from each cgroup's cpu.stat,
|
||||
so an overload outside every container can be named (novox/hq issue 741). It changes nothing and
|
||||
declares no resources, so it can be assigned to every machine.
|
||||
|
||||
## Why it exists, and why here
|
||||
|
||||
@@ -22,7 +24,7 @@ machine's resources a seat, these tools become its verbs.
|
||||
|
||||
## Tools
|
||||
|
||||
All four only read.
|
||||
All five only read.
|
||||
|
||||
| tool | what | replaces |
|
||||
|---|---|---|
|
||||
@@ -30,6 +32,7 @@ All four only read.
|
||||
| `disk_top` | Over a window. `by=unit` (the default) ranks systemd units and containers by bytes moved, from their cgroups. `by=container` ranks containers by name, and `by=process` ranks processes | `iotop` |
|
||||
| `disk_filesystems` | Each filesystem that stores data, said once however many places it is mounted: size, used and free GiB, used % as `df` counts it, inodes used % | `df -h`, `df -i` |
|
||||
| `disk_devices` | Each disk without sampling: model, size, whether it spins, scheduler, queue limit, what it is built on, and since boot the GiB read and written, the busy share and the wait per read and per write | `lsblk`, `cat /proc/diskstats` |
|
||||
| `cpu_top` | Samples a window (`seconds`, default 3, at most 10). It answers the machine's CPU in use (% of all CPUs and as CPUs busy, from `/proc/stat`), then `by=unit` (the default) ranks systemd units and containers by CPU, from their cgroups' cpu.stat, busiest first (`limit`, default 15), with the busy time no unit accounts for; `by=container` ranks containers by name | `top`, `systemd-cgtop` |
|
||||
|
||||
### How each number is read
|
||||
|
||||
|
||||
@@ -68,9 +68,10 @@ func (m *Machine) bottomDevices() map[string]bool {
|
||||
return out
|
||||
}
|
||||
|
||||
// unitIO is every unit's storage I/O, by its cgroup's path: the innermost .service or .scope, because
|
||||
// io.stat counts a cgroup's descendants too and an outer unit would count them twice.
|
||||
func (m *Machine) unitIO(counted map[string]bool) map[string]cgroupIO {
|
||||
// unitPaths is every systemd unit's cgroup path under /sys/fs/cgroup: the innermost .service or
|
||||
// .scope, because a cgroup's own stat file counts its descendants too and an outer unit would count
|
||||
// them twice. Shared by every reader that walks the cgroup tree (unitIO here, unitCPU in cpu.go).
|
||||
func (m *Machine) unitPaths() []string {
|
||||
base := m.path("/sys/fs/cgroup")
|
||||
var units []string
|
||||
filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error {
|
||||
@@ -89,11 +90,19 @@ func (m *Machine) unitIO(counted map[string]bool) map[string]cgroupIO {
|
||||
outer[dir] = true
|
||||
}
|
||||
}
|
||||
out := map[string]cgroupIO{}
|
||||
out := make([]string, 0, len(units))
|
||||
for _, u := range units {
|
||||
if outer[u] {
|
||||
continue
|
||||
if !outer[u] {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// unitIO is every unit's storage I/O, by its cgroup's path.
|
||||
func (m *Machine) unitIO(counted map[string]bool) map[string]cgroupIO {
|
||||
out := map[string]cgroupIO{}
|
||||
for _, u := range m.unitPaths() {
|
||||
text := m.read("/sys/fs/cgroup/" + u + "/io.stat")
|
||||
if text == "" {
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// cgroupCPU is a cgroup's processor time, from its cpu.stat.
|
||||
type cgroupCPU struct {
|
||||
usageUsec uint64
|
||||
}
|
||||
|
||||
// parseCPUStat reads a cgroup's cpu.stat ("usage_usec 123\nuser_usec …\nsystem_usec …"), readable by
|
||||
// every account, unlike /proc/<pid>/stat for another account's process.
|
||||
func parseCPUStat(s string) cgroupCPU {
|
||||
var c cgroupCPU
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
k, v, ok := strings.Cut(line, " ")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if k == "usage_usec" {
|
||||
c.usageUsec, _ = strconv.ParseUint(strings.TrimSpace(v), 10, 64)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// unitCPU is every unit's processor time, by its cgroup's path. A cgroup whose cpu.stat cannot be
|
||||
// read is left out, never an error for the whole answer.
|
||||
func (m *Machine) unitCPU() map[string]cgroupCPU {
|
||||
out := map[string]cgroupCPU{}
|
||||
for _, u := range m.unitPaths() {
|
||||
text := m.read("/sys/fs/cgroup/" + u + "/cpu.stat")
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
out[u] = parseCPUStat(text)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MachineCPU is the machine's CPU in use over the window, from /proc/stat's cpu line. CPUsBusy is
|
||||
// rounded only for display; raw, unexported, is the same count unrounded, for topCPU's share and
|
||||
// unaccounted math (review round 1 of novox/hq issue 741: a small busy time that rounds CPUsBusy to
|
||||
// 0.0 must not zero out every unit's share along with it).
|
||||
type MachineCPU struct {
|
||||
CPUs int `json:"cpus"`
|
||||
BusyPct float64 `json:"busy_pct"` // % of all CPUs together
|
||||
CPUsBusy float64 `json:"cpus_busy"` // as a count of CPUs, e.g. 7.5 of 12, rounded for display
|
||||
raw float64
|
||||
}
|
||||
|
||||
// machineCPUOver is the machine's CPU in use between two readings of /proc/stat: everything but idle
|
||||
// and iowait, counted against the window's total ticks over every CPU.
|
||||
func machineCPUOver(a, b cpuTimes) MachineCPU {
|
||||
total := float64(b.total() - a.total())
|
||||
if total <= 0 {
|
||||
return MachineCPU{CPUs: b.cpus}
|
||||
}
|
||||
idle := sub(b.idle, a.idle) + sub(b.iowait, a.iowait)
|
||||
busy := total - idle
|
||||
if busy < 0 {
|
||||
busy = 0
|
||||
}
|
||||
raw := busy / total * float64(b.cpus)
|
||||
return MachineCPU{CPUs: b.cpus, BusyPct: round1(busy / total * 100), CPUsBusy: round1(raw), raw: raw}
|
||||
}
|
||||
|
||||
// CPUUsage is a unit's or a container's share of CPU between two readings of its cgroup's cpu.stat.
|
||||
// Cgroup is its cgroup's path, relative to /sys/fs/cgroup, said beside Unit always (review round 1 of
|
||||
// novox/hq issue 741): two cgroups can share a leaf name (an outer "init.scope" and a user session's
|
||||
// "user.slice/user@1000.service/init.scope"), and the path is how they are told apart.
|
||||
type CPUUsage struct {
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Cgroup string `json:"cgroup,omitempty"`
|
||||
Container string `json:"container,omitempty"`
|
||||
CPUPct float64 `json:"cpu_pct"` // % of one CPU: 100 = one CPU busy the whole window
|
||||
SharePct float64 `json:"share_pct"` // this unit's share of the machine's busy time
|
||||
raw float64
|
||||
}
|
||||
|
||||
// topCPU ranks the units, or the containers, with the most CPU time between two readings of
|
||||
// cpu.stat, busiest first. unaccountedPct is the machine's busy time no unit accounts for, as a
|
||||
// share of it (never below 0): the sum is always taken over every unit, whichever by ranks by, so a
|
||||
// limit on the list never changes it.
|
||||
func (m *Machine) topCPU(a, b map[string]cgroupCPU, window time.Duration, by string, limit int, cpusBusy float64) ([]CPUUsage, float64) {
|
||||
windowUsec := window.Seconds() * 1e6
|
||||
type entry struct {
|
||||
path, unit, container string
|
||||
pct float64
|
||||
}
|
||||
var entries []entry
|
||||
var names map[string]string
|
||||
var sumPct float64
|
||||
for path, end := range b {
|
||||
start, ok := a[path]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
d := sub(end.usageUsec, start.usageUsec)
|
||||
if d == 0 {
|
||||
continue
|
||||
}
|
||||
pct := d / windowUsec * 100
|
||||
sumPct += pct
|
||||
unit := filepath.Base(path)
|
||||
var container string
|
||||
if id := containerID.FindStringSubmatch("/" + path); id != nil {
|
||||
if names == nil {
|
||||
names = m.containerNames()
|
||||
}
|
||||
container = id[1][:12]
|
||||
if n, ok := names[id[1]]; ok {
|
||||
container = n
|
||||
}
|
||||
}
|
||||
entries = append(entries, entry{path: path, unit: unit, container: container, pct: pct})
|
||||
}
|
||||
|
||||
// Units are keyed by their cgroup's own path, never by its leaf name (filepath.Base): two cgroups can
|
||||
// share a leaf name (an outer "init.scope" and a user session's nested one), and keying by the name
|
||||
// alone would merge them into one row (review round 1 of novox/hq issue 741). Containers stay keyed
|
||||
// by their name, as before.
|
||||
groups := map[string]*CPUUsage{}
|
||||
for _, e := range entries {
|
||||
key := e.path
|
||||
if by == "container" {
|
||||
if e.container == "" {
|
||||
continue
|
||||
}
|
||||
key = e.container
|
||||
}
|
||||
g, ok := groups[key]
|
||||
if !ok {
|
||||
g = &CPUUsage{Unit: e.unit, Cgroup: e.path, Container: e.container}
|
||||
if by == "container" {
|
||||
g.Unit, g.Cgroup = "", ""
|
||||
}
|
||||
groups[key] = g
|
||||
}
|
||||
g.raw += e.pct
|
||||
}
|
||||
out := make([]CPUUsage, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
out = append(out, *g)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].raw != out[j].raw {
|
||||
return out[i].raw > out[j].raw
|
||||
}
|
||||
return out[i].Unit+out[i].Cgroup+out[i].Container < out[j].Unit+out[j].Cgroup+out[j].Container
|
||||
})
|
||||
if len(out) > limit {
|
||||
out = out[:limit]
|
||||
}
|
||||
for i := range out {
|
||||
out[i].CPUPct = round1(out[i].raw)
|
||||
if cpusBusy > 0 {
|
||||
out[i].SharePct = round1(out[i].raw / (cpusBusy * 100) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
unaccounted := cpusBusy*100 - sumPct
|
||||
if unaccounted < 0 {
|
||||
unaccounted = 0
|
||||
}
|
||||
unaccountedPct := 0.0
|
||||
if cpusBusy > 0 {
|
||||
unaccountedPct = round1(unaccounted / (cpusBusy * 100) * 100)
|
||||
}
|
||||
return out, unaccountedPct
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// aCPUHog is a machine whose CPU is 75 % busy (1.5 of 2 CPUs) over a 2 s window: a unit, another unit,
|
||||
// and a container, each with a known usage_usec delta, leaving part of the machine's busy time
|
||||
// unaccounted — the shape of issue 739, where 750 % of 12 CPUs went to processes outside containers.
|
||||
func aCPUHog(t *testing.T) *fake {
|
||||
f := newFake(t)
|
||||
f.file("/proc/stat", "cpu 0 0 0 0 0 0 0 0 0 0\ncpu0 0 0 0 0 0 0 0 0 0 0\ncpu1 0 0 0 0 0 0 0 0 0 0\nintr 1\n")
|
||||
f.then("/proc/stat", "cpu 100 0 50 50 0 0 0 0 0 0\ncpu0 50 0 25 25 0 0 0 0 0 0\ncpu1 50 0 25 25 0 0 0 0 0 0\nintr 2\n")
|
||||
|
||||
zero := "usage_usec 0\nuser_usec 0\nsystem_usec 0\n"
|
||||
cg := func(path string, usec int) {
|
||||
f.file("/sys/fs/cgroup/"+path+"/cpu.stat", zero)
|
||||
f.then("/sys/fs/cgroup/"+path+"/cpu.stat", "usage_usec "+itoa(usec)+"\nuser_usec "+itoa(usec)+"\nsystem_usec 0\n")
|
||||
}
|
||||
cg("system.slice/alpha.service", 1000000) // 50 % of one CPU over 2 s
|
||||
cg("system.slice/beta.service", 400000) // 20 %
|
||||
cg("system.slice/docker-"+store+".scope", 600000) // 30 %, named "cpu-hog" by the runtime
|
||||
f.answers["docker ps --no-trunc --format {{.ID}}\t{{.Names}}"] = store + "\tcpu-hog\n"
|
||||
return f
|
||||
}
|
||||
|
||||
func TestCPUTopRanksUnitsAndContainersByCPUOverTheWindow(t *testing.T) {
|
||||
f := aCPUHog(t)
|
||||
out, err := tool(t, f.machine(), "cpu_top").Run(map[string]any{"seconds": 2.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := out.(map[string]any)
|
||||
if m["by"] != "unit" || f.waited != 2*time.Second {
|
||||
t.Errorf("the default: %+v, waited %s", m, f.waited)
|
||||
}
|
||||
cpu := m["cpu"].(MachineCPU)
|
||||
if cpu.CPUs != 2 || cpu.BusyPct != 75 || cpu.CPUsBusy != 1.5 {
|
||||
t.Errorf("machine cpu %+v", cpu)
|
||||
}
|
||||
top := m["top"].([]CPUUsage)
|
||||
if len(top) != 3 {
|
||||
t.Fatalf("three units moved: %+v", top)
|
||||
}
|
||||
if top[0].Unit != "alpha.service" || top[0].CPUPct != 50 || top[0].SharePct != 33.3 {
|
||||
t.Errorf("busiest first: %+v", top[0])
|
||||
}
|
||||
if top[1].Container != "cpu-hog" || top[1].CPUPct != 30 || top[1].SharePct != 20 {
|
||||
t.Errorf("the container, named by the runtime: %+v", top[1])
|
||||
}
|
||||
if top[2].Unit != "beta.service" || top[2].CPUPct != 20 || top[2].SharePct != 13.3 {
|
||||
t.Errorf("the least busy: %+v", top[2])
|
||||
}
|
||||
if m["unaccounted_pct"] != 33.3 {
|
||||
t.Errorf("unaccounted: %+v (machine busy 150%% of one CPU, units sum to 100%%, 50%% of 150%% left over)", m["unaccounted_pct"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCPUTopByContainerNamesOnlyContainers(t *testing.T) {
|
||||
out, err := tool(t, aCPUHog(t).machine(), "cpu_top").Run(map[string]any{"by": "container", "seconds": 2.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
top := out.(map[string]any)["top"].([]CPUUsage)
|
||||
if len(top) != 1 || top[0].Container != "cpu-hog" || top[0].Unit != "" || top[0].CPUPct != 30 {
|
||||
t.Errorf("by container: %+v", top)
|
||||
}
|
||||
if _, err := tool(t, aCPUHog(t).machine(), "cpu_top").Run(map[string]any{"by": "process"}); err == nil {
|
||||
t.Error("by=process was accepted; cpu_top only ranks units and containers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCPUTopLimitsHowManyAreNamed(t *testing.T) {
|
||||
out, err := tool(t, aCPUHog(t).machine(), "cpu_top").Run(map[string]any{"limit": 1.0, "seconds": 2.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := out.(map[string]any)
|
||||
top := m["top"].([]CPUUsage)
|
||||
if len(top) != 1 || top[0].Unit != "alpha.service" {
|
||||
t.Errorf("limit 1: %+v", top)
|
||||
}
|
||||
// The limit narrows the list, never the unaccounted share, which is taken over every unit.
|
||||
if m["unaccounted_pct"] != 33.3 {
|
||||
t.Errorf("unaccounted with limit 1: %+v", m["unaccounted_pct"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCPUTopLeavesOutACgroupThatAppearsOrVanishesWithinTheWindow(t *testing.T) {
|
||||
f := aCPUHog(t)
|
||||
f.then("/sys/fs/cgroup/system.slice/short-lived.service/cpu.stat", "usage_usec 500000\n")
|
||||
out, err := tool(t, f.machine(), "cpu_top").Run(map[string]any{"seconds": 2.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
top := out.(map[string]any)["top"].([]CPUUsage)
|
||||
for _, u := range top {
|
||||
if u.Unit == "short-lived.service" {
|
||||
t.Errorf("a unit only in the second reading was counted: %+v", top)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(strings.Join(f.calls, ","), "docker ps") {
|
||||
t.Errorf("the container runtime was never asked: %v", f.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// aTinyLoad is a machine with 12 CPUs, only 0.04 of them busy over a 2 s window (busy 1 of 300 total
|
||||
// ticks): rounded to one decimal for display, CPUsBusy reads 0.0, but a unit using half of that busy
|
||||
// time still has a non-zero share — review round 1's decision that share_pct and unaccounted_pct are
|
||||
// computed from the unrounded busy time, never from the rounded CPUsBusy.
|
||||
func aTinyLoad(t *testing.T) *fake {
|
||||
f := newFake(t)
|
||||
cpus := "cpu0 0 0 0 0 0 0 0 0 0 0\ncpu1 0 0 0 0 0 0 0 0 0 0\ncpu2 0 0 0 0 0 0 0 0 0 0\ncpu3 0 0 0 0 0 0 0 0 0 0\n" +
|
||||
"cpu4 0 0 0 0 0 0 0 0 0 0\ncpu5 0 0 0 0 0 0 0 0 0 0\ncpu6 0 0 0 0 0 0 0 0 0 0\ncpu7 0 0 0 0 0 0 0 0 0 0\n" +
|
||||
"cpu8 0 0 0 0 0 0 0 0 0 0\ncpu9 0 0 0 0 0 0 0 0 0 0\ncpu10 0 0 0 0 0 0 0 0 0 0\ncpu11 0 0 0 0 0 0 0 0 0 0\n"
|
||||
f.file("/proc/stat", "cpu 0 0 0 0 0 0 0 0 0 0\n"+cpus)
|
||||
f.then("/proc/stat", "cpu 1 0 0 299 0 0 0 0 0 0\n"+cpus)
|
||||
|
||||
f.file("/sys/fs/cgroup/system.slice/tiny.service/cpu.stat", "usage_usec 0\n")
|
||||
f.then("/sys/fs/cgroup/system.slice/tiny.service/cpu.stat", "usage_usec 60000\n") // 3 % of one CPU over 2 s
|
||||
return f
|
||||
}
|
||||
|
||||
func TestCPUTopSharesAreNonZeroEvenWhenRoundedCPUsBusyIsZero(t *testing.T) {
|
||||
out, err := tool(t, aTinyLoad(t).machine(), "cpu_top").Run(map[string]any{"seconds": 2.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := out.(map[string]any)
|
||||
cpu := m["cpu"].(MachineCPU)
|
||||
if cpu.CPUs != 12 || cpu.CPUsBusy != 0 {
|
||||
t.Fatalf("the machine 0.04 CPUs busy, rounded for display: %+v", cpu)
|
||||
}
|
||||
top := m["top"].([]CPUUsage)
|
||||
if len(top) != 1 || top[0].Unit != "tiny.service" || top[0].CPUPct != 3 || top[0].SharePct != 75 {
|
||||
t.Fatalf("a non-zero share from the unrounded busy time: %+v", top)
|
||||
}
|
||||
if m["unaccounted_pct"] != 25.0 {
|
||||
t.Fatalf("unaccounted from the unrounded busy time: %+v (want 25, so shares plus unaccounted add to 100)",
|
||||
m["unaccounted_pct"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestCPUTopKeepsCgroupsWithTheSameUnitNameAsSeparateRows is review round 1's other decision: units are keyed
|
||||
// by their cgroup path, never by filepath.Base, so two cgroups that both end in "init.scope" — one at the
|
||||
// root, one under a user's session — are two rows, not one merged by their shared leaf name.
|
||||
func TestCPUTopKeepsCgroupsWithTheSameUnitNameAsSeparateRows(t *testing.T) {
|
||||
f := aCPUHog(t)
|
||||
cg := func(path string, usec int) {
|
||||
f.file("/sys/fs/cgroup/"+path+"/cpu.stat", "usage_usec 0\n")
|
||||
f.then("/sys/fs/cgroup/"+path+"/cpu.stat", "usage_usec "+itoa(usec)+"\n")
|
||||
}
|
||||
cg("init.scope", 1000000) // the root init, 50 % of one CPU over 2 s
|
||||
cg("user.slice/user@1000.service/init.scope", 400000) // a user session's init, 20 %
|
||||
|
||||
out, err := tool(t, f.machine(), "cpu_top").Run(map[string]any{"seconds": 2.0, "limit": 10.0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
top := out.(map[string]any)["top"].([]CPUUsage)
|
||||
var inits []CPUUsage
|
||||
for _, u := range top {
|
||||
if u.Unit == "init.scope" {
|
||||
inits = append(inits, u)
|
||||
}
|
||||
}
|
||||
if len(inits) != 2 {
|
||||
t.Fatalf("two cgroups named init.scope, merged into %d row(s): %+v", len(inits), top)
|
||||
}
|
||||
byCgroup := map[string]CPUUsage{}
|
||||
for _, u := range inits {
|
||||
byCgroup[u.Cgroup] = u
|
||||
}
|
||||
if u, ok := byCgroup["init.scope"]; !ok || u.CPUPct != 50 {
|
||||
t.Errorf("the root init.scope: %+v", byCgroup)
|
||||
}
|
||||
if u, ok := byCgroup["user.slice/user@1000.service/init.scope"]; !ok || u.CPUPct != 20 {
|
||||
t.Errorf("the user session's init.scope: %+v", byCgroup)
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,57 @@ func Tools(m *Machine) []stdio.Tool {
|
||||
return out, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cpu_top",
|
||||
Description: "What used the most CPU over a few seconds, from each unit's cgroup cpu.stat, which needs no root: the machine's " +
|
||||
"CPU in use (as % of all CPUs and as CPUs busy, from /proc/stat), then by=unit (the default) each systemd unit and " +
|
||||
"container ranked by CPU, busiest first, with the busy time no unit accounts for; by=container only the containers, " +
|
||||
"by name. Replaces top, systemd-cgtop. (r)",
|
||||
Input: map[string]any{
|
||||
"seconds": map[string]any{"type": "integer", "description": "how long to sample, in seconds (default 3, at most 10)"},
|
||||
"limit": map[string]any{"type": "integer", "description": "how many units or containers to name (default 15, at most 50)"},
|
||||
"by": map[string]any{"type": "string", "enum": []string{"unit", "container"}, "description": "what to rank (default unit)"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
n, err := count(args, "seconds", 3, 1, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w := time.Duration(n) * time.Second
|
||||
limit, err := count(args, "limit", 15, 1, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
by := text(args, "by")
|
||||
switch by {
|
||||
case "":
|
||||
by = "unit"
|
||||
case "unit", "container":
|
||||
default:
|
||||
return nil, fmt.Errorf("by is unit or container, not %q", by)
|
||||
}
|
||||
ac, err := parseCPU(m.read("/proc/stat"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := m.unitCPU()
|
||||
m.Wait(w)
|
||||
bc, err := parseCPU(m.read("/proc/stat"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := m.unitCPU()
|
||||
machine := machineCPUOver(ac, bc)
|
||||
top, unaccounted := m.topCPU(a, b, w, by, limit, machine.raw)
|
||||
return map[string]any{
|
||||
"window_s": w.Seconds(),
|
||||
"by": by,
|
||||
"cpu": machine,
|
||||
"top": top,
|
||||
"unaccounted_pct": unaccounted,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disk_filesystems",
|
||||
Description: "The space of every filesystem that stores data (not proc, tmpfs or overlay layers): size, used and free in GiB, " +
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"disk_load",
|
||||
"disk_top",
|
||||
"disk_filesystems",
|
||||
"disk_devices"
|
||||
"disk_devices",
|
||||
"cpu_top"
|
||||
],
|
||||
"build": {
|
||||
"artifacts": [
|
||||
|
||||
Reference in New Issue
Block a user