System information
The machine a program runs on: CPU usage per core measured between readings, memory and swap with cgroup limits, disks with free space and the one holding a path, network totals and rates, load per core, uptime, processes sorted and searched, and a health check that turns limits into warnings.
Version 1.0.0 · 14 functions · 2 crates
Functions
What use system_info.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use system_info_cpu, system_info_memory from system_info (Quillo 0.6).
system_info_cpu() → anyor an errorThe CPUs: { brand, arch, logical, physical, usage_percent, frequency_mhz, per_core }. The first call takes 200 ms to measure; later ones report since the one before.
system_info_memory() → anyor an errorMemory and swap in bytes, with used_percent and sizes in GiB, and the container's limits under Linux cgroups in cgroup.
system_info_disks(options: any) → listor an errorThe mounted disks: { name, mount, file_system, kind, total, available, used_percent, removable, read_only }, largest first. options.real_only leaves out pseudo file systems and read-only snapshots.
system_info_disk_for(path: str) → anyor an errorThe disk holding a path, the longest mount that contains it, with its free space: what a "disk almost full" check wants.
system_info_network() → listor an errorNetwork interfaces since boot, sorted by traffic: { name, received, transmitted, packets_received, packets_transmitted, errors, mac, addresses }, and rate_in and rate_out in bytes a second since the last call (null the first time).
system_info_load() → anyor an errorLoad averages over 1, 5 and 15 minutes, with per_core, the first divided by the logical CPUs: over 1 means work is waiting.
system_info_uptime() → anyHow long the machine has been up: { seconds, boot_time, text }, text like "3 days, 4:05".
system_info_os() → anyThe operating system and host: { name, version, long_version, kernel, host, arch }, for an about page.
system_info_summary() → anyor an errorAll at once, for a status page: { os, cpu, memory, disks, load, uptime }.
system_info_processes(options: any) → listor an errorProcesses, heaviest first: options sort ("cpu", "memory", "name" or "pid"), limit (20), and name, part of a name to look for. Each has pid, parent, name, cpu_percent, memory, status, run_seconds, user and command. What a process's command line says is its own business: it may hold secrets.
system_info_process(pid: int) → any?or an errorOne process by its pid, null when it has gone.
system_info_self() → any?or an errorThis program's own process: pid, memory, CPU and run time.
system_info_process_counts() → anyor an errorHow many processes there are, and how many in each state.
system_info_health(limits: any) → anyor an errorWarnings for a health check: { ok, warnings }, from limits such as { memory_percent: 90, disk_percent: 90, load_per_core: 2.0 }. The disks looked at are the real ones.
Get it
$ quillo install system_info # system_info.qwer into this folder$ quillo install system_info --example # and its example, system_info.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/system_info.qwer$ curl -O https://quillolang.org/plugins/files/system_info.qweThen, in your program, all of it or only what you call:
use system_info.qweruse system_info_cpu, system_info_memory from system_info // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
sysinfo = { version = "=0.37.2", default-features = false, features = ["disk", "network", "system", "user"] }serde_json = { version = "1", features = ["preserve_order"] }Example
// A status page for the machine this runs on: CPU, memory, disks, network, load and processes,// and a health check that says what is wrong. Numbers differ on every machine, so the routes// here show that they make sense together.//// quillo run system_info.qwe then open http://localhost:8080/ and /healthuse "system_info.qwer"route GET / = system_info_summary()?route GET /cpu { let c = system_info_cpu()? { has_brand: len(c.brand ?? "") > 0, logical_ok: c.logical >= 1, cores_match: len(c.per_core) == c.logical, usage_ok: c.usage_percent >= 0 && c.usage_percent <= 100, physical_ok: c.physical == null || c.physical <= c.logical, arch_ok: len(c.arch) > 0 }}route GET /memory { let m = system_info_memory()? { total_ok: m.total > 1048576, used_within: m.used <= m.total, percent_ok: m.used_percent > 0 && m.used_percent <= 100, gib_ok: m.total_gib > 0 }}route GET /disks { let all = system_info_disks({})? let real = system_info_disks({ real_only: true })? let here = system_info_disk_for(".")? { listed: len(all) >= 1, real_ok: len(real) >= 1 && len(real) <= len(all), sensible: len(real | filter((d) => d.available > d.total)) == 0, sorted: real[0].total >= real[len(real) - 1].total, here_ok: here.total > 0, no_path: system_info_disk_for("/no/such/path").error != null }}route GET /network { let first = system_info_network()? sleep(100) let second = system_info_network()? { interfaces: len(first) >= 1, named: len(first | filter((i) => len(i.name) == 0)) == 0, rates_later: len(second | filter((i) => i.rate_in == null)) == 0 }}route GET /load { let l = system_info_load()? let u = system_info_uptime() { load_ok: l.one >= 0, per_core_ok: l.per_core >= 0, up: u.seconds > 0, boot_ok: u.boot_time > 1000000000, text_ok: contains(u.text, ":") }}route GET /os { let o = system_info_os() { named: o.name != null, kernel: o.kernel != null, host: o.host != null }}route GET /processes { let top = system_info_processes({ sort: "memory", limit: 5 })? let me = system_info_self()? let found = system_info_processes({ name: "quillo", limit: 50 })? let counts = system_info_process_counts()? { five: len(top), sorted: top[0].memory >= top[4].memory, has_fields: top[0].pid > 0 && len(top[0].name) > 0, me_found: me.pid > 0 && contains(me.name, "quillo"), me_memory: me.memory > 0, by_name: len(found) >= 1, by_pid: system_info_process(me.pid)?.pid == me.pid, gone: system_info_process(4000000)?, counted: counts.total >= 5, bad_sort: system_info_processes({ sort: "age" }).error, bad_pid: system_info_process(-1).error, }}route GET /health { let calm = system_info_health({ memory_percent: 101, disk_percent: 101, load_per_core: 100000.0 })? let tight = system_info_health({ memory_percent: 0, disk_percent: 0 })? { calm: calm, warned: !tight.ok, says_memory: contains(tight.warnings[0], "memory is"), says_disk: len(tight.warnings) >= 2 && contains(tight.warnings[1], "% full") }}$ quillo run system_info.qwe$ curl 'localhost:8080/cpu'$ curl 'localhost:8080/memory'$ curl 'localhost:8080/disks'$ curl 'localhost:8080/network'$ curl 'localhost:8080/load'$ curl 'localhost:8080/os'$ curl 'localhost:8080/processes'$ curl 'localhost:8080/health'$ curl 'localhost:8080/'Source
//! The machine a program runs on (`sysinfo`): CPU usage per core, memory and swap, disks with free space, network totals and rates, load average, uptime, and processes sorted by CPU or memory, for status pages and health checks.//! version: 1.0.0//!//! [dependencies]//! sysinfo = { version = "=0.37.2", default-features = false, features = ["disk", "network", "system", "user"] }//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};use std::sync::{Mutex, OnceLock};use std::time::Instant;use sysinfo::{DiskKind, Disks, Networks, Pid, Process, ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind, Users, MINIMUM_CPU_UPDATE_INTERVAL};struct Keep { sys: System, cpu_at: Option<Instant>, processes_at: Option<Instant>, net: Networks, net_at: Option<Instant>,}fn keep() -> &'static Mutex<Keep> { static K: OnceLock<Mutex<Keep>> = OnceLock::new(); K.get_or_init(|| Mutex::new(Keep { sys: System::new(), cpu_at: None, processes_at: None, net: Networks::new_with_refreshed_list(), net_at: None }))}fn percent(x: f64) -> f64 { (x * 10.0).round() / 10.0}fn gib(b: u64) -> f64 { (b as f64 / (1u64 << 30) as f64 * 100.0).round() / 100.0}// CPU usage is the change between two readings, so the first ever waits for a second one, and// readings closer than the system's interval are the same reading.fn refresh_cpu(k: &mut Keep) { match k.cpu_at { None => { k.sys.refresh_cpu_usage(); std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL); k.sys.refresh_cpu_usage(); k.cpu_at = Some(Instant::now()); } Some(t) if t.elapsed() >= MINIMUM_CPU_UPDATE_INTERVAL => { k.sys.refresh_cpu_usage(); k.cpu_at = Some(Instant::now()); } _ => {} }}/// The CPUs: `{ brand, arch, logical, physical, usage_percent, frequency_mhz, per_core }`. The first call takes 200 ms to measure; later ones report since the one before.pub fn system_info_cpu() -> Result<Value, String> { let mut k = keep().lock().map_err(|_| "lock")?; k.sys.refresh_cpu_all(); refresh_cpu(&mut k); let cpus = k.sys.cpus(); Ok(json!({ "brand": cpus.first().map(|c| c.brand().trim().to_string()), "arch": System::cpu_arch(), "logical": cpus.len(), "physical": System::physical_core_count(), "usage_percent": percent(k.sys.global_cpu_usage() as f64), "frequency_mhz": cpus.first().map(|c| c.frequency()), "per_core": cpus.iter().map(|c| percent(c.cpu_usage() as f64)).collect::<Vec<_>>(), }))}/// Memory and swap in bytes, with `used_percent` and sizes in GiB, and the container's limits under Linux cgroups in `cgroup`.pub fn system_info_memory() -> Result<Value, String> { let mut k = keep().lock().map_err(|_| "lock")?; k.sys.refresh_memory(); let (total, used, avail) = (k.sys.total_memory(), k.sys.used_memory(), k.sys.available_memory()); let mut m = json!({ "total": total, "used": used, "free": k.sys.free_memory(), "available": avail, "used_percent": if total == 0 { 0.0 } else { percent(used as f64 * 100.0 / total as f64) }, "total_gib": gib(total), "available_gib": gib(avail), "swap_total": k.sys.total_swap(), "swap_used": k.sys.used_swap(), }); if let Some(c) = k.sys.cgroup_limits() { m["cgroup"] = json!({ "total": c.total_memory, "free": c.free_memory, "rss": c.rss }); } Ok(m)}/// The mounted disks: `{ name, mount, file_system, kind, total, available, used_percent, removable, read_only }`, largest first. `options.real_only` leaves out pseudo file systems and read-only snapshots.pub fn system_info_disks(options: serde_json::Value) -> Result<Vec<Value>, String> { let real = options.get("real_only").and_then(|v| v.as_bool()).unwrap_or(false); let disks = Disks::new_with_refreshed_list(); let mut l: Vec<_> = disks.list().iter().filter(|d| d.total_space() > 0).filter(|d| !real || (!d.is_read_only() && !matches!(d.file_system().to_string_lossy().as_ref(), "devfs" | "tmpfs" | "overlay" | "squashfs"))).collect(); l.sort_by_key(|d| std::cmp::Reverse(d.total_space())); Ok(l.into_iter().map(|d| { let (t, a) = (d.total_space(), d.available_space()); json!({ "name": d.name().to_string_lossy(), "mount": d.mount_point().display().to_string(), "file_system": d.file_system().to_string_lossy(), "kind": match d.kind() { DiskKind::HDD => "hdd".to_string(), DiskKind::SSD => "ssd".to_string(), DiskKind::Unknown(_) => "unknown".to_string() }, "total": t, "available": a, "total_gib": gib(t), "available_gib": gib(a), "used_percent": percent(t.saturating_sub(a) as f64 * 100.0 / t as f64), "removable": d.is_removable(), "read_only": d.is_read_only(), }) }).collect())}/// The disk holding a path, the longest mount that contains it, with its free space: what a "disk almost full" check wants.pub fn system_info_disk_for(path: &str) -> Result<Value, String> { let p = std::fs::canonicalize(path).map_err(|e| format!("{}: {}", path, e))?; let disks = Disks::new_with_refreshed_list(); let d = disks.list().iter().filter(|d| p.starts_with(d.mount_point())).max_by_key(|d| d.mount_point().as_os_str().len()).ok_or(format!("no mounted disk holds {}", path))?; let (t, a) = (d.total_space(), d.available_space()); Ok(json!({ "mount": d.mount_point().display().to_string(), "total": t, "available": a, "available_gib": gib(a), "used_percent": if t == 0 { 0.0 } else { percent(t.saturating_sub(a) as f64 * 100.0 / t as f64) } }))}/// Network interfaces since boot, sorted by traffic: `{ name, received, transmitted, packets_received, packets_transmitted, errors, mac, addresses }`, and `rate_in` and `rate_out` in bytes a second since the last call (null the first time).pub fn system_info_network() -> Result<Vec<Value>, String> { let mut k = keep().lock().map_err(|_| "lock")?; let since = k.net_at.map(|t| t.elapsed().as_secs_f64()); k.net.refresh(true); k.net_at = Some(Instant::now()); let mut l: Vec<_> = k.net.list().iter().collect(); l.sort_by(|a, b| (b.1.total_received() + b.1.total_transmitted()).cmp(&(a.1.total_received() + a.1.total_transmitted())).then(a.0.cmp(b.0))); Ok(l.into_iter().map(|(n, d)| { let rate = |x: u64| since.filter(|s| *s > 0.0).map(|s| (x as f64 / s).round()); json!({ "name": n, "received": d.total_received(), "transmitted": d.total_transmitted(), "packets_received": d.total_packets_received(), "packets_transmitted": d.total_packets_transmitted(), "errors": d.total_errors_on_received() + d.total_errors_on_transmitted(), "rate_in": rate(d.received()), "rate_out": rate(d.transmitted()), "mac": d.mac_address().to_string(), "addresses": d.ip_networks().iter().map(|i| format!("{}/{}", i.addr, i.prefix)).collect::<Vec<_>>(), }) }).collect())}/// Load averages over 1, 5 and 15 minutes, with `per_core`, the first divided by the logical CPUs: over 1 means work is waiting.pub fn system_info_load() -> Result<Value, String> { let l = System::load_average(); let mut k = keep().lock().map_err(|_| "lock")?; if k.sys.cpus().is_empty() { k.sys.refresh_cpu_list(sysinfo::CpuRefreshKind::nothing()); } let n = k.sys.cpus().len().max(1); Ok(json!({ "one": l.one, "five": l.five, "fifteen": l.fifteen, "per_core": (l.one / n as f64 * 100.0).round() / 100.0 }))}/// How long the machine has been up: `{ seconds, boot_time, text }`, `text` like "3 days, 4:05".pub fn system_info_uptime() -> Value { let s = System::uptime(); let (d, h, m) = (s / 86400, s % 86400 / 3600, s % 3600 / 60); let text = if d > 0 { format!("{} day{}, {}:{:02}", d, if d == 1 { "" } else { "s" }, h, m) } else { format!("{}:{:02}", h, m) }; json!({ "seconds": s, "boot_time": System::boot_time(), "text": text })}/// The operating system and host: `{ name, version, long_version, kernel, host, arch }`, for an about page.pub fn system_info_os() -> Value { json!({ "name": System::name(), "version": System::os_version(), "long_version": System::long_os_version(), "kernel": System::kernel_version(), "host": System::host_name(), "arch": System::cpu_arch() })}/// All at once, for a status page: `{ os, cpu, memory, disks, load, uptime }`.pub fn system_info_summary() -> Result<Value, String> { Ok(json!({ "os": system_info_os(), "cpu": system_info_cpu()?, "memory": system_info_memory()?, "disks": system_info_disks(json!({ "real_only": true }))?, "load": system_info_load()?, "uptime": system_info_uptime() }))}fn users() -> &'static Mutex<Users> { static U: OnceLock<Mutex<Users>> = OnceLock::new(); U.get_or_init(|| Mutex::new(Users::new_with_refreshed_list()))}fn process_map(p: &Process, u: &Users) -> Value { json!({ "pid": p.pid().as_u32(), "parent": p.parent().map(|x| x.as_u32()), "name": p.name().to_string_lossy(), "cpu_percent": percent(p.cpu_usage() as f64), "memory": p.memory(), "memory_mib": (p.memory() as f64 / (1u64 << 20) as f64 * 10.0).round() / 10.0, "status": p.status().to_string(), "run_seconds": p.run_time(), "user": p.user_id().and_then(|i| u.get_user_by_id(i)).map(|x| x.name().to_string()), "command": p.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect::<Vec<_>>().join(" "), })}fn refresh_processes(k: &mut Keep) { let kind = ProcessRefreshKind::nothing().with_cpu().with_memory().with_user(UpdateKind::OnlyIfNotSet).with_cmd(UpdateKind::OnlyIfNotSet); // Like the CPUs, a process's CPU share is the change between two looks. let first = k.processes_at.is_none(); if first { k.sys.refresh_processes_specifics(ProcessesToUpdate::All, true, kind); std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL); } k.sys.refresh_cpu_usage(); k.sys.refresh_processes_specifics(ProcessesToUpdate::All, true, kind); k.processes_at = Some(Instant::now());}/// Processes, heaviest first: options `sort` ("cpu", "memory", "name" or "pid"), `limit` (20), and `name`, part of a name to look for. Each has `pid`, `parent`, `name`, `cpu_percent`, `memory`, `status`, `run_seconds`, `user` and `command`. What a process's command line says is its own business: it may hold secrets.pub fn system_info_processes(options: serde_json::Value) -> Result<Vec<Value>, String> { let sort = options.get("sort").and_then(|v| v.as_str()).unwrap_or("cpu"); if !["cpu", "memory", "name", "pid"].contains(&sort) { return Err(format!("`{}` is not a sort: cpu, memory, name or pid", sort)); } let limit = options.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; let want = options.get("name").and_then(|v| v.as_str()).map(|s| s.to_lowercase()); let mut k = keep().lock().map_err(|_| "lock")?; refresh_processes(&mut k); let mut l: Vec<&Process> = k.sys.processes().values().filter(|p| p.thread_kind().is_none()).filter(|p| want.as_ref().map_or(true, |w| p.name().to_string_lossy().to_lowercase().contains(w))).collect(); match sort { "cpu" => l.sort_by(|a, b| b.cpu_usage().total_cmp(&a.cpu_usage()).then(b.memory().cmp(&a.memory()))), "memory" => l.sort_by_key(|p| std::cmp::Reverse(p.memory())), "name" => l.sort_by_key(|p| p.name().to_string_lossy().to_lowercase()), _ => l.sort_by_key(|p| p.pid()), } let u = users().lock().map_err(|_| "lock")?; Ok(l.into_iter().take(limit).map(|p| process_map(p, &u)).collect())}/// One process by its pid, null when it has gone.pub fn system_info_process(pid: i64) -> Result<Option<Value>, String> { let n = u32::try_from(pid).map_err(|_| format!("{} is not a pid", pid))?; let mut k = keep().lock().map_err(|_| "lock")?; refresh_processes(&mut k); let u = users().lock().map_err(|_| "lock")?; Ok(k.sys.process(Pid::from_u32(n)).map(|p| process_map(p, &u)))}/// This program's own process: pid, memory, CPU and run time.pub fn system_info_self() -> Result<Option<Value>, String> { system_info_process(std::process::id() as i64)}/// How many processes there are, and how many in each state.pub fn system_info_process_counts() -> Result<Value, String> { let mut k = keep().lock().map_err(|_| "lock")?; refresh_processes(&mut k); let mut by = std::collections::BTreeMap::new(); let mut total = 0; for p in k.sys.processes().values().filter(|p| p.thread_kind().is_none()) { total += 1; *by.entry(p.status().to_string()).or_insert(0u64) += 1; } Ok(json!({ "total": total, "by_status": by }))}/// Warnings for a health check: `{ ok, warnings }`, from `limits` such as `{ memory_percent: 90, disk_percent: 90, load_per_core: 2.0 }`. The disks looked at are the real ones.pub fn system_info_health(limits: serde_json::Value) -> Result<Value, String> { let n = |k: &str| limits.get(k).and_then(|v| v.as_f64()); let mut warn = vec![]; if let Some(l) = n("memory_percent") { let u = system_info_memory()?["used_percent"].as_f64().unwrap_or(0.0); if u >= l { warn.push(format!("memory is {}% used", u)); } } if let Some(l) = n("disk_percent") { for d in system_info_disks(json!({ "real_only": true }))? { let u = d["used_percent"].as_f64().unwrap_or(0.0); if u >= l { warn.push(format!("{} is {}% full", d["mount"].as_str().unwrap_or("?"), u)); } } } if let Some(l) = n("load_per_core") { let u = system_info_load()?["per_core"].as_f64().unwrap_or(0.0); if u >= l { warn.push(format!("load is {} per core", u)); } } Ok(json!({ "ok": warn.is_empty(), "warnings": warn }))}Related
uptime_check.qwer · 1.0.0Uptime checks for a status page: whether a host's port is open and how fast, a URL's status and response time, many at once.8 views · 8 downloadsLog parser log_parse.qwer · 1.0.0Log lines into rows: nginx and Apache access logs, syslog, logfmt and JSON lines, the format found by itself, with counts by any field.7 views · 7 downloadsPrometheus metrics prometheus.qwer · 1.0.0Counters, gauges and histograms with labels kept in memory, and the /metrics text Prometheus and Grafana scrape.8 views · 8 downloadsSource maps source_maps.qwer · 1.0.0Minified JavaScript stack traces mapped back to the original files, lines, columns and function names with their source maps, and the code around each place.7 views · 8 downloadsPJL print jobs and status pjl.qwer · 1.0.0PDF, PostScript and PCL files printed on office printers without a driver, wrapped in PJL with copies, duplex, tray, held jobs released by PIN; and the printer asked for its status, model and page count, its codes told in words.6 views · 7 downloadsPrinter status printer_status.qwer · 1.0.0Any printer asked how it is in its own language, Zebra, PJL, TSPL, ESC/POS or Brother, or by SNMP for toner levels, with one clear answer: ready, paper out, ribbon out, head open, paused or jammed.9 views · 7 downloadsThread 0 posts
On System information: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.