diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/db.rs | 69 | ||||
-rw-r--r-- | src/gatherer.rs | 71 | ||||
-rwxr-xr-x | src/main.rs | 6 |
3 files changed, 143 insertions, 3 deletions
diff --git a/src/db.rs b/src/db.rs index f10f9ee..9a0256a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -4,7 +4,7 @@ use camino::Utf8PathBuf; use rusqlite::{params, Connection, OptionalExtension}; use time::OffsetDateTime; -use crate::gatherer::Meminfo; +use crate::gatherer::{Cpuinfo, Meminfo}; pub struct Database { db_path: Utf8PathBuf, @@ -23,6 +23,7 @@ impl Database { let conn = self.conn.lock().unwrap(); conn.execute(CREATE_TABLE_HOSTMEM, params![]).unwrap(); conn.execute(CREATE_TABLE_HOSTNET, params![]).unwrap(); + conn.execute(CREATE_TABLE_HOSTCPU, params![]).unwrap(); } pub fn insert_host_meminfo(&self, meminfo: Meminfo) { @@ -125,6 +126,42 @@ impl Database { .map(|r| r.unwrap()) .collect() } + + pub fn insert_hostcpu( + &self, + span_sec: usize, + user_delta: usize, + nice_delta: usize, + system_delta: usize, + ) { + let conn = self.conn.lock().unwrap(); + + conn.execute( + "INSERT INTO stats_hostcpu(span_sec, user_delta, nice_delta, system_delta) VALUES (?1, ?2, ?3, ?4)", + params![span_sec, user_delta, nice_delta, system_delta], + ) + .unwrap(); + } + + pub fn get_last_n_hostcpu(&self, count: usize) -> Vec<DbCpuinfo> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn + .prepare("SELECT * FROM stats_hostcpu ORDER BY stamp DESC LIMIT ?1") + .unwrap(); + + stmt.query_map(params![count], |row| { + Ok(DbCpuinfo { + stamp: row.get(0)?, + span_sec: row.get(1)?, + user_delta: row.get(2)?, + nice_delta: row.get(3)?, + system_delta: row.get(4)?, + }) + }) + .unwrap() + .map(|r| r.unwrap()) + .collect() + } } pub const CREATE_TABLE_HOSTMEM: &'static str = "\ @@ -142,6 +179,15 @@ pub const CREATE_TABLE_HOSTNET: &'static str = "\ tx_delta INTEGER NOT NULL );"; +pub const CREATE_TABLE_HOSTCPU: &'static str = "\ + CREATE TABLE IF NOT EXISTS stats_hostcpu( + stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + span_sec INTEGER NOT NULL, + user_delta INTEGER NOT NULL, + nice_delta INTEGER NOT NULL, + system_delta INTEGER NOT NULL + );"; + #[derive(Clone, Copy, Debug)] pub struct DbMeminfo { pub stamp: OffsetDateTime, @@ -172,3 +218,24 @@ impl DbNetinfo { self.tx_delta as f32 / self.span_sec as f32 } } + +#[derive(Copy, Clone, Debug)] +pub struct DbCpuinfo { + pub stamp: OffsetDateTime, + pub span_sec: usize, + pub user_delta: usize, + pub nice_delta: usize, + pub system_delta: usize, +} + +impl DbCpuinfo { + /// Returns the avarage usage still in USER_HZ which is + /// in 1/100ths sec on "most systems". This is averaged + /// across the minute sample rate, so unit is: + /// USER_HZ/s + pub fn average_usage(&self) -> f32 { + let used_sum = self.user_delta + self.nice_delta + self.system_delta; + let avg = used_sum as f32 / self.span_sec as f32; + avg + } +} diff --git a/src/gatherer.rs b/src/gatherer.rs index 163d84b..7289f60 100644 --- a/src/gatherer.rs +++ b/src/gatherer.rs @@ -40,6 +40,7 @@ fn task(state: AwakeState) { // I just want a graph on first boot; don't care about divisions just yet make_mem_graph(&state); make_net_graph(&state); + make_cpu_graph(&state); // If we collected a point less than a minute ago, like after // just being restarted, wait until it's been a minute @@ -56,6 +57,7 @@ fn task(state: AwakeState) { } let mut last_netinfo: Option<Netinfo> = None; + let mut last_cpuinfo: Option<Cpuinfo> = None; // this is a `let _` because otherwise the attribute was // making the comiler mad @@ -66,11 +68,14 @@ fn task(state: AwakeState) { // Gather data let meminfo = Meminfo::current(); let netinfo = Netinfo::current(); + let cpuinfo = Cpuinfo::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)); + tracing::trace!("cpu: user {} // nice {} // system {}", cpuinfo.user, cpuinfo.nice, cpuinfo.system); + // Store stats in database if let Some(lni) = last_netinfo { let rx_delta = netinfo.rx_bytes - lni.rx_bytes; let tx_delta = netinfo.tx_bytes - lni.tx_bytes; @@ -79,7 +84,15 @@ fn task(state: AwakeState) { } last_netinfo = Some(netinfo); - // Store stats in database + if let Some(lci) = last_cpuinfo { + let user_delta = cpuinfo.user - lci.user; + let nice_delta = cpuinfo.nice - lci.nice; + let system_delta = cpuinfo.system - lci.system; + + state.database.insert_hostcpu(60, user_delta, nice_delta, system_delta); + } + last_cpuinfo = Some(cpuinfo); + state.database.insert_host_meminfo(meminfo); // Only generate graphs every 15 minutes @@ -87,6 +100,7 @@ fn task(state: AwakeState) { if now.minute() % 15 == 0 { make_mem_graph(&state); make_net_graph(&state); + make_cpu_graph(&state); } std::thread::sleep(Duration::from_secs(60)); @@ -159,6 +173,26 @@ pub fn make_net_graph(state: &AwakeState) { gif.save(path).unwrap(); } +pub fn make_cpu_graph(state: &AwakeState) { + tracing::debug!("generating cpuinfo graph"); + + let infos = state.database.get_last_n_hostcpu(256); + + let now = OffsetDateTime::now_utc(); + let cleaned = clean_series(&infos, |cpu| cpu.stamp, now); + // Scalling by 10 because the graph system does not like to have a max of only 100 due to some bad programming + let mut usages = extract(&cleaned, |cpu| (cpu.average_usage() * 10.0) as usize); + + // Reversing here because we want latest valeus on on the + // right side, so last in the array + usages.reverse(); + + let gif = griph::make_1line(0, 1000, &usages); + + let path = state.cache_path.join("current_hostcpuinfo.gif"); + gif.save(path).unwrap(); +} + fn clean_series<T, F>(series: &[T], time_extractor: F, end_time: OffsetDateTime) -> [Option<T>; 256] where F: Fn(&T) -> OffsetDateTime, @@ -292,6 +326,41 @@ impl Netinfo { } } +/// Not strictly the correct name for this, but that's fine. +/// Structure for parsed-from /proc/stat +pub struct Cpuinfo { + user: usize, + nice: usize, + system: usize, +} + +impl Cpuinfo { + pub fn current() -> Self { + let procinfo = File::open("/proc/stat").unwrap(); + let bread = BufReader::new(procinfo); + + let mut cpuinfo = Cpuinfo { + user: 0, + nice: 0, + system: 0, + }; + + for line in bread.lines() { + let line = line.unwrap(); + let Some(data) = line.strip_prefix("cpu ") else { + continue; + }; + + let mut splits = data.split(' '); + cpuinfo.user = splits.next().unwrap().parse().unwrap(); + cpuinfo.nice = splits.next().unwrap().parse().unwrap(); + cpuinfo.system = splits.next().unwrap().parse().unwrap(); + } + + cpuinfo + } +} + fn data_human_fmt(bytes: usize) -> String { let (num, unit) = data_human(bytes); format!("{num}{unit}") diff --git a/src/main.rs b/src/main.rs index 6d0f27e..5ba279f 100755 --- a/src/main.rs +++ b/src/main.rs @@ -443,7 +443,11 @@ fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: String } async fn stats(State(state): State<AwakeState>, Path(name): Path<String>) -> Response { - const NAMES: &[&'static str] = &["current_hostmeminfo.gif", "current_hostnetinfo.gif"]; + const NAMES: &[&'static str] = &[ + "current_hostmeminfo.gif", + "current_hostnetinfo.gif", + "current_hostcpuinfo.gif", + ]; if NAMES.contains(&name.as_str()) { let path = state.cache_path.join(name); |