173 lines
6.0 KiB
Go
173 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Tools is the module's tools over one machine. Every one only reads.
|
|
func Tools(m *Machine) []stdio.Tool {
|
|
window := map[string]any{"type": "integer", "description": "how long to sample, in seconds (default 2, at most 10)"}
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "disk_load",
|
|
Description: "How loaded this machine's storage is, sampled over a few seconds: the share of CPU time spent waiting on I/O (iowait), " +
|
|
"I/O pressure stall (PSI), and for each disk how busy it was (%), read and write throughput (MiB/s), IOPS, the average wait " +
|
|
"per request (ms) and the queue depth, with the filesystems it holds; then the filesystems' space and the processes " +
|
|
"and the units moving the most bytes (from their cgroups), with their container. Each finding is said in a line under `said`. Replaces iostat, vmstat, " +
|
|
"iotop and df. (r)",
|
|
Input: map[string]any{
|
|
"seconds": window,
|
|
"processes": map[string]any{"type": "integer", "description": "how many of the busiest units and processes to name (default 5, 0 for none, at most 50)"},
|
|
"all": map[string]any{"type": "boolean", "description": "every block device and partition, not only the whole disks (default false)"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
w, err := seconds(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := count(args, "processes", 5, 0, 50)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.LoadOver(w, n, flag(args, "all"), true)
|
|
},
|
|
},
|
|
{
|
|
Name: "disk_top",
|
|
Description: "What moved the most bytes to and from storage over a few seconds: by=unit (the default) each systemd unit " +
|
|
"and container from its cgroup's io.stat, with MiB/s and IOPS, complete whoever runs the tools; by=container only the " +
|
|
"containers, by name; by=process each process from /proc/<pid>/io, which misses another account's unless the tool " +
|
|
"runner is root. Replaces iotop. (r)",
|
|
Input: map[string]any{
|
|
"seconds": window,
|
|
"limit": map[string]any{"type": "integer", "description": "how many (default 10, at most 50)"},
|
|
"by": map[string]any{"type": "string", "enum": []string{"process", "unit", "container"}, "description": "what to rank (default unit)"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
w, err := seconds(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
limit, err := count(args, "limit", 10, 1, 50)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
by := text(args, "by")
|
|
switch by {
|
|
case "":
|
|
by = "unit"
|
|
case "process", "unit", "container":
|
|
default:
|
|
return nil, fmt.Errorf("by is process, unit or container, not %q", by)
|
|
}
|
|
out := map[string]any{"window_s": w.Seconds(), "by": by}
|
|
if by != "process" {
|
|
counted := m.bottomDevices()
|
|
a := m.unitIO(counted)
|
|
m.Wait(w)
|
|
out["top"] = m.topUnits(a, m.unitIO(counted), w, by, limit)
|
|
return out, nil
|
|
}
|
|
a, _ := m.processIO()
|
|
m.Wait(w)
|
|
b, unreadable := m.processIO()
|
|
out["top"] = m.topProcesses(a, b, w, limit)
|
|
if unreadable > 0 {
|
|
out["processes_unreadable"] = unreadable
|
|
out["note"] = unreadableNote(unreadable)
|
|
}
|
|
return out, 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, " +
|
|
"used % as df counts it, and inodes used %. A filesystem mounted at several places is said once. Replaces df. (r)",
|
|
Run: func(map[string]any) (any, error) {
|
|
fss := m.Filesystems()
|
|
said := []string{}
|
|
for _, fs := range fss {
|
|
switch {
|
|
case fs.Unanswered != "":
|
|
said = append(said, fmt.Sprintf("%s: not read (%s)", fs.Mount, fs.Unanswered))
|
|
default:
|
|
line := fmt.Sprintf("%s (%s, %s): %.1f %% used, %.1f GiB free of %.1f GiB", fs.Mount, fs.Type, fs.Source, fs.UsedPct, fs.AvailGiB, fs.SizeGiB)
|
|
if fs.UsedPct >= fullLine || fs.InodesUsed >= fullLine {
|
|
line += " — NEARLY FULL"
|
|
}
|
|
said = append(said, line)
|
|
}
|
|
}
|
|
return map[string]any{"said": said, "filesystems": fss}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "disk_devices",
|
|
Description: "Each disk as it is, without sampling: model, size (GiB), whether it spins, its I/O scheduler and queue limit, what " +
|
|
"it is built on (for LVM, encryption, RAID), and since the machine started: GiB read and written, the share of the time it " +
|
|
"was busy and the average wait per read and per write (ms). Replaces lsblk and cat /proc/diskstats. (r)",
|
|
Input: map[string]any{
|
|
"all": map[string]any{"type": "boolean", "description": "every block device and partition, not only the whole disks (default false)"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
ds, up := m.DevicesNow(flag(args, "all"))
|
|
return map[string]any{"up_hours": up, "devices": ds}, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func seconds(args map[string]any) (time.Duration, error) {
|
|
n, err := count(args, "seconds", 2, 1, 10)
|
|
return time.Duration(n) * time.Second, err
|
|
}
|
|
|
|
func text(args map[string]any, key string) string {
|
|
s, _ := args[key].(string)
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
func flag(args map[string]any, key string) bool {
|
|
switch v := args[key].(type) {
|
|
case bool:
|
|
return v
|
|
case string:
|
|
return v == "true"
|
|
}
|
|
return false
|
|
}
|
|
|
|
// count is a whole-number argument, defaulted, refused below least, held at most.
|
|
func count(args map[string]any, key string, fallback, least, most int) (int, error) {
|
|
v, given := args[key]
|
|
if !given || v == nil {
|
|
return fallback, nil
|
|
}
|
|
var n int
|
|
switch x := v.(type) {
|
|
case float64:
|
|
if x != math.Trunc(x) {
|
|
return 0, fmt.Errorf("%s must be a whole number, not %v", key, x)
|
|
}
|
|
n = int(x)
|
|
case string:
|
|
i, err := strconv.Atoi(strings.TrimSpace(x))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be a whole number, not %q", key, x)
|
|
}
|
|
n = i
|
|
default:
|
|
return 0, fmt.Errorf("%s must be a whole number", key)
|
|
}
|
|
if n < least {
|
|
return 0, fmt.Errorf("%s must be at least %d", key, least)
|
|
}
|
|
return min(n, most), nil
|
|
}
|