diff options
Diffstat (limited to 'src/gatherer.rs')
-rw-r--r-- | src/gatherer.rs | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/src/gatherer.rs b/src/gatherer.rs new file mode 100644 index 0000000..0cceea6 --- /dev/null +++ b/src/gatherer.rs @@ -0,0 +1,228 @@ +use std::{ + fs::File, + io::{BufRead, BufReader}, + sync::mpsc::Sender, + thread::JoinHandle, + time::Duration, +}; + +use regex_lite::Regex; +use time::OffsetDateTime; + +use crate::{griph, AwakeState}; + +pub struct Gatherer { + state: AwakeState, + hwnd: Option<JoinHandle<()>>, +} + +impl Gatherer { + pub fn new(state: AwakeState) -> Self { + Self { state, hwnd: None } + } + + pub fn start(&mut self) { + let state = self.state.clone(); + let hwnd = std::thread::spawn(|| task(state)); + self.hwnd = Some(hwnd); + } +} + +fn task(state: AwakeState) { + tracing::info!("starting gatherer thread"); + + // I just want a graph on first boot; don't care about divisions just yet + make_mem_graph(&state); + make_net_graph(&state); + + let mut last_netinfo: Option<Netinfo> = None; + + // this is a `let _` because otherwise the attribute was + // making the comiler mad + #[rustfmt::skip] + let _ = loop { + tracing::debug!("collecting stats"); + + // Gather data + let meminfo = Meminfo::current(); + let netinfo = Netinfo::current(); + + // Print traces, y'know, for tracing + tracing::trace!("memory: {}MB used / {}MB total", meminfo.usage() / 1000, meminfo.total / 1000); + tracing::trace!("net: rx {} / tx {}", data_human_fmt(netinfo.rx_bytes), data_human_fmt(netinfo.tx_bytes)); + + if let Some(lni) = last_netinfo { + let rx_delta = netinfo.rx_bytes - lni.rx_bytes; + let tx_delta = netinfo.tx_bytes - lni.tx_bytes; + + state.database.insert_hostnet(60, rx_delta, tx_delta); + } + last_netinfo = Some(netinfo); + + // Store stats in database + state.database.insert_host_meminfo(meminfo); + + // Only generate graphs every 15 minutes + let now = OffsetDateTime::now_utc(); + if now.minute() % 15 == 0 { + make_mem_graph(&state); + make_net_graph(&state); + } + + std::thread::sleep(Duration::from_secs(60)); + }; +} + +pub fn make_mem_graph(state: &AwakeState) { + tracing::debug!("generating meminfo graph"); + + let infos = state.database.get_last_n_host_meminfo(256); + let max = infos[0].total_kb; + let usages: Vec<usize> = infos.into_iter().map(|mi| mi.usage()).collect(); + + let gif = griph::make_1line(0, max, &usages); + + let path = state.cache_path.join("current_hostmeminfo.gif"); + gif.save(path).unwrap(); +} + +pub fn make_net_graph(state: &AwakeState) { + tracing::debug!("generating netinfo graph"); + + let infos = state.database.get_last_n_hostnet(256); + let rx_deltas: Vec<usize> = infos + .iter() + .map(|ni| ni.rx_bytes_per_sec() as usize / 1000) + .collect(); + let tx_deltas: Vec<usize> = infos + .iter() + .map(|ni| ni.tx_bytes_per_sec() as usize / 1000) + .collect(); + + for ahh in &tx_deltas { + tracing::trace!("ahh: {ahh} kbytes"); + } + + let gif = griph::make_2line(0, 1000, &rx_deltas, &tx_deltas); + + let path = state.cache_path.join("current_hostnetinfo.gif"); + gif.save(path).unwrap(); +} + +pub struct Meminfo { + pub total: usize, + pub free: usize, + pub avaialable: usize, +} + +impl Meminfo { + pub fn current() -> Self { + let procinfo = File::open("/proc/meminfo").unwrap(); + let bread = BufReader::new(procinfo); + + let mut meminfo = Meminfo { + total: 0, + free: 0, + avaialable: 0, + }; + + for line in bread.lines() { + let line = line.unwrap(); + + if let Some((raw_key, raw_value_kb)) = line.split_once(':') { + let value = if let Some(raw_value) = raw_value_kb.trim().strip_suffix(" kB") { + if let Ok(parsed) = raw_value.parse() { + parsed + } else { + continue; + } + } else { + continue; + }; + + match raw_key.trim() { + "MemTotal" => meminfo.total = value, + "MemFree" => meminfo.free = value, + "MemAvailable" => meminfo.avaialable = value, + _ => (), + } + } + } + + meminfo + } + + pub fn usage(&self) -> usize { + self.total - self.avaialable + } +} + +pub struct Netinfo { + rx_bytes: usize, + tx_bytes: usize, +} + +impl Netinfo { + pub fn current() -> Self { + let procinfo = File::open("/proc/net/dev").unwrap(); + let bread = BufReader::new(procinfo); + + let mut netinfo = Self { + rx_bytes: 0, + tx_bytes: 0, + }; + + let re = Regex::new(r"[ ]*(\d+)").unwrap(); + let interface = "eth0:"; + for line in bread.lines() { + let line = line.unwrap(); + let trim = line.trim(); + + let mut captures = if let Some(data) = trim.strip_prefix(interface) { + re.captures_iter(data) + } else { + continue; + }; + + netinfo.rx_bytes = captures + .next() + .unwrap() + .get(1) + .unwrap() + .as_str() + .parse() + .unwrap(); + netinfo.tx_bytes = captures + .skip(7) + .next() + .unwrap() + .get(1) + .unwrap() + .as_str() + .parse() + .unwrap(); + break; + } + + netinfo + } +} + +fn data_human_fmt(bytes: usize) -> String { + let (num, unit) = data_human(bytes); + format!("{num}{unit}") +} + +fn data_human(bytes: usize) -> (f32, &'static str) { + const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB"]; + + let mut wrk = bytes as f32; + let mut unit_idx = 0; + loop { + if wrk < 1500.0 || unit_idx == UNITS.len() - 1 { + return (wrk, UNITS[unit_idx]); + } + wrk /= 1000.0; + unit_idx += 1; + } +} |