about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-02-16 10:17:28 -0600
committergennyble <gen@nyble.dev>2025-02-16 10:17:28 -0600
commit0c5986851dee23e09751ae594d8a43a84a0dab61 (patch)
treeed27d132aa04b86ecabd9cf5bb440b7d87f7bff3 /src
parente1b51d7dfc24c443af34410da2fed6aa6db52b62 (diff)
downloadawake-0c5986851dee23e09751ae594d8a43a84a0dab61.tar.gz
awake-0c5986851dee23e09751ae594d8a43a84a0dab61.zip
Network stats
Diffstat (limited to 'src')
-rw-r--r--src/db.rs191
-rw-r--r--src/gatherer.rs228
-rw-r--r--src/griph/mod.rs98
-rwxr-xr-xsrc/main.rs113
4 files changed, 530 insertions, 100 deletions
diff --git a/src/db.rs b/src/db.rs
index d4038cb..6edeba1 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -4,44 +4,127 @@ use camino::Utf8PathBuf;
 use rusqlite::{params, Connection, OptionalExtension};
 use time::OffsetDateTime;
 
-use crate::Meminfo;
+use crate::gatherer::Meminfo;
 
-
-pub struct Database{ 
-    db_path: Utf8PathBuf,
-    conn: Mutex<Connection>
+pub struct Database {
+	db_path: Utf8PathBuf,
+	conn: Mutex<Connection>,
 }
 
 impl Database {
-    pub fn new(db_path: Utf8PathBuf) -> Self {
-        Self {
-            conn:Mutex::new(Connection::open(&db_path).unwrap()),
-            db_path,
-        }
-    }
-
-    pub fn create_tables(&self) {
-        let conn = self.conn.lock().unwrap();
-        conn.execute(CREATE_TABLE_HOSTMEM, params![]).unwrap();
-    }
-
-    pub fn insert_host_meminfo(&self, meminfo: Meminfo) {
-        let conn = self.conn.lock().unwrap();
-
-        conn.execute("INSERT INTO stats_hostmem(total_kb, available_kb) VALUES (?1, ?2)", params![meminfo.total, meminfo.avaialable]).unwrap();
-    }
-
-    pub fn get_last_host_meminfo(&self) -> DbMeminfo {
-        let conn = self.conn.lock().unwrap();
-
-        conn.query_row("SELECT * FROM stats_hostmem ORDER BY stamp DESC LIMIT 1", [], |row| {
-            let (stamp, total_kb, available_kb) = row.try_into().unwrap();
-
-            Ok(DbMeminfo {
-                stamp, total_kb, available_kb
-            })
-        }).optional().unwrap().unwrap()
-    }
+	pub fn new(db_path: Utf8PathBuf) -> Self {
+		Self {
+			conn: Mutex::new(Connection::open(&db_path).unwrap()),
+			db_path,
+		}
+	}
+
+	pub fn create_tables(&self) {
+		let conn = self.conn.lock().unwrap();
+		conn.execute(CREATE_TABLE_HOSTMEM, params![]).unwrap();
+		conn.execute(CREATE_TABLE_HOSTNET, params![]).unwrap();
+	}
+
+	pub fn insert_host_meminfo(&self, meminfo: Meminfo) {
+		let conn = self.conn.lock().unwrap();
+
+		conn.execute(
+			"INSERT INTO stats_hostmem(total_kb, available_kb) VALUES (?1, ?2)",
+			params![meminfo.total, meminfo.avaialable],
+		)
+		.unwrap();
+	}
+
+	pub fn get_last_host_meminfo(&self) -> DbMeminfo {
+		let conn = self.conn.lock().unwrap();
+
+		conn.query_row(
+			"SELECT * FROM stats_hostmem ORDER BY stamp DESC LIMIT 1",
+			[],
+			|row| {
+				let (stamp, total_kb, available_kb) = row.try_into().unwrap();
+
+				Ok(DbMeminfo {
+					stamp,
+					total_kb,
+					available_kb,
+				})
+			},
+		)
+		.optional()
+		.unwrap()
+		.unwrap()
+	}
+
+	pub fn get_last_n_host_meminfo(&self, count: usize) -> Vec<DbMeminfo> {
+		let conn = self.conn.lock().unwrap();
+		let mut stmt = conn
+			.prepare("SELECT * FROM stats_hostmem ORDER BY stamp ASC LIMIT ?1")
+			.unwrap();
+
+		stmt.query_map(params![count], |row| {
+			Ok(DbMeminfo {
+				stamp: row.get(0)?,
+				total_kb: row.get(1)?,
+				available_kb: row.get(2)?,
+			})
+		})
+		.unwrap()
+		.map(|r| r.unwrap())
+		.collect()
+	}
+
+	pub fn get_date_bound_n_host_meminfo(
+		&self,
+		since: OffsetDateTime,
+		until: OffsetDateTime,
+		count: usize,
+	) -> Vec<DbMeminfo> {
+		let conn = self.conn.lock().unwrap();
+		let mut stmt = conn
+			.prepare("SELECT * FROM stats_hostmem WHERE stamp > ?1 AND stamp < ?2 ORDER BY stamp ASC LIMIT ?3")
+			.unwrap();
+
+		stmt.query_map(params![since, until, count], |row| {
+			Ok(DbMeminfo {
+				stamp: row.get(0)?,
+				total_kb: row.get(1)?,
+				available_kb: row.get(2)?,
+			})
+		})
+		.unwrap()
+		.map(|r| r.unwrap())
+		.collect()
+	}
+
+	pub fn insert_hostnet(&self, span_sec: usize, rx_delta: usize, tx_delta: usize) {
+		let conn = self.conn.lock().unwrap();
+
+		conn.execute(
+			"INSERT INTO stats_hostnet(span_sec, rx_delta, tx_delta) VALUES (?1, ?2, ?3)",
+			params![span_sec, rx_delta, tx_delta],
+		)
+		.unwrap();
+	}
+
+	pub fn get_last_n_hostnet(&self, count: usize) -> Vec<DbNetinfo> {
+		let conn = self.conn.lock().unwrap();
+		let mut stmt = conn
+			.prepare("SELECT * FROM stats_hostnet ORDER BY stamp ASC LIMIT ?1")
+			.unwrap();
+
+		stmt.query_map(params![count], |row| {
+			Ok(DbNetinfo {
+				stamp: row.get(0)?,
+				span_sec: row.get(1)?,
+				rx_delta: row.get(2)?,
+				tx_delta: row.get(3)?,
+			})
+		})
+		.unwrap()
+		.map(|r| r.unwrap())
+		.collect()
+	}
 }
 
 pub const CREATE_TABLE_HOSTMEM: &'static str = "\
@@ -51,14 +134,40 @@ pub const CREATE_TABLE_HOSTMEM: &'static str = "\
         available_kb INTEGER NOT NULL
     );";
 
+pub const CREATE_TABLE_HOSTNET: &'static str = "\
+    CREATE TABLE IF NOT EXISTS stats_hostnet(
+        stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        span_sec INTEGER NOT NULL,
+        rx_delta INTEGER NOT NULL,
+        tx_delta INTEGER NOT NULL
+    );";
+
 pub struct DbMeminfo {
-    pub stamp: OffsetDateTime,
-    pub total_kb: usize,
-    pub available_kb: usize
+	pub stamp: OffsetDateTime,
+	pub total_kb: usize,
+	pub available_kb: usize,
 }
 
 impl DbMeminfo {
-    pub fn usage(&self) -> usize {
-        self.total_kb - self.available_kb
-    }
-}
\ No newline at end of file
+	pub fn usage(&self) -> usize {
+		self.total_kb - self.available_kb
+	}
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct DbNetinfo {
+	pub stamp: OffsetDateTime,
+	pub span_sec: usize,
+	pub rx_delta: usize,
+	pub tx_delta: usize,
+}
+
+impl DbNetinfo {
+	pub fn rx_bytes_per_sec(&self) -> f32 {
+		self.rx_delta as f32 / self.span_sec as f32
+	}
+
+	pub fn tx_bytes_per_sec(&self) -> f32 {
+		self.tx_delta as f32 / self.span_sec as f32
+	}
+}
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;
+	}
+}
diff --git a/src/griph/mod.rs b/src/griph/mod.rs
new file mode 100644
index 0000000..b034207
--- /dev/null
+++ b/src/griph/mod.rs
@@ -0,0 +1,98 @@
+use gifed::{writer::ImageBuilder, Gif, StandardGif};
+
+#[rustfmt::skip]
+pub const DARK_PALETTE: &[u8] = &[
+	  0,   0,   0, // Background - Black
+	192, 192, 192, // Graphline - Mostly White
+	 64,  64,  64, // Gridlines - Dark gray
+	 32,  32,  32, // Minor Gridlines - Darker gray
+	 48,  48, 192, // Primary 2 Colour - Blue
+	 48, 192,  48, // Secondary 2 Colour - Green
+];
+
+const BACKGROUND: u8 = 0;
+const LINE: u8 = 1;
+const GRIDLINE: u8 = 2;
+const MINOR_GRIDLINE: u8 = 3;
+const LINE1: u8 = 4;
+const LINE2: u8 = 5;
+
+const WIDTH: usize = 256;
+const HEIGHT: usize = 160;
+const SIZE: usize = WIDTH * HEIGHT;
+
+pub fn make_1line(min: usize, max: usize, values: &[usize]) -> Gif {
+	let range = max - min;
+	// this assumes a range of values that is >1 per pixel
+	let vpp = range / HEIGHT;
+
+	let mut raster = vec![0; SIZE];
+	draw_grid(&mut raster);
+	draw_line(&mut raster, values, vpp, LINE);
+
+	let mut standard = Gif::new(WIDTH as u16, HEIGHT as u16);
+	standard.set_palette(Some(DARK_PALETTE[0..12].try_into().unwrap()));
+	standard.push(
+		ImageBuilder::new(WIDTH as u16, HEIGHT as u16)
+			.build(raster)
+			.unwrap(),
+	);
+
+	standard
+}
+
+pub fn make_2line(min: usize, max: usize, values1: &[usize], values2: &[usize]) -> Gif {
+	let range = max - min;
+	// this assumes a range of values that is >1 per pixel
+	let vpp = range / HEIGHT;
+
+	let mut raster = vec![0; SIZE];
+	draw_grid(&mut raster);
+	draw_line(&mut raster, values1, vpp, LINE1);
+	draw_line(&mut raster, values2, vpp, LINE2);
+
+	let mut standard = Gif::new(WIDTH as u16, HEIGHT as u16);
+	standard.set_palette(Some(DARK_PALETTE.try_into().unwrap()));
+	standard.push(
+		ImageBuilder::new(WIDTH as u16, HEIGHT as u16)
+			.build(raster)
+			.unwrap(),
+	);
+
+	standard
+}
+
+fn draw_grid(raster: &mut [u8]) {
+	// Draw Divisions
+	// we want a gridline every 16 pixels, but not the bottom
+	// or top, so only 8.
+	for div in 1..=9 {
+		let y_val = div * 16;
+
+		let grid = if div % 2 == 0 {
+			GRIDLINE
+		} else {
+			MINOR_GRIDLINE
+		};
+
+		for x in 0..WIDTH {
+			raster[y_val * WIDTH + x] = grid;
+		}
+	}
+}
+
+fn draw_line(raster: &mut [u8], values: &[usize], vpp: usize, colour: u8) {
+	// Draw Line
+	// this will be discontinuous and i think that's okay. we
+	// could make it a proper line by keeping track what value
+	// was last and drawing the whole vertical there
+	for (x, value) in values.iter().enumerate() {
+		let value_height = value / vpp;
+		if value_height > (HEIGHT - 1) {
+			continue;
+		}
+		let y_val = (HEIGHT - 1) - value_height;
+
+		raster[y_val * WIDTH + x] = colour;
+	}
+}
diff --git a/src/main.rs b/src/main.rs
index 26e1cf0..cddd2f1 100755
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,16 +1,26 @@
 mod atom;
+mod db;
 mod error;
 mod fs;
+mod gatherer;
+mod griph;
 mod ifc;
 mod markup;
 mod settings;
 mod templated;
 mod timeparse;
 mod util;
-mod db;
 
-use std::{fs::File, io::{BufRead, BufReader, Write}, os::unix::fs::MetadataExt, str::FromStr, sync::Arc, time::Duration};
+use std::{
+	fs::File,
+	io::{BufRead, BufReader, Write},
+	os::unix::fs::MetadataExt,
+	str::FromStr,
+	sync::Arc,
+	time::Duration,
+};
 
+use ::time::OffsetDateTime;
 use axum::{
 	body::Body,
 	extract::{Path, State},
@@ -25,6 +35,7 @@ use confindent::{Confindent, Node};
 use db::Database;
 pub use error::RuntimeError;
 use fs::Filesystem;
+use gatherer::Gatherer;
 use settings::Settings;
 use templated::Frontmatter;
 use tokio_util::io::ReaderStream;
@@ -38,7 +49,8 @@ use crate::{
 
 #[derive(Clone)]
 pub struct AwakeState {
-	pub database: Arc<Database>
+	pub database: Arc<Database>,
+	pub cache_path: Utf8PathBuf,
 }
 
 #[tokio::main]
@@ -64,6 +76,7 @@ async fn main() {
 	let templates = conf.child_value("Templates").unwrap();
 	let hostname = conf.child_owned("Hostname").unwrap();
 	let dbpath = conf.child_owned("Database").unwrap();
+	let cache = conf.child_owned("Cache").unwrap();
 
 	let database = Database::new(dbpath.into());
 	database.create_tables();
@@ -71,18 +84,24 @@ async fn main() {
 	let fs = Filesystem::new(&webroot);
 
 	let state = AwakeState {
-		database: Arc::new(database)
+		database: Arc::new(database),
+		cache_path: cache.into(),
 	};
 
-	let mi_state = state.clone();
-	let meminfo_thread = std::thread::spawn(move || {
-		loop {
-			let meminfo = Meminfo::current();
-			mi_state.database.insert_host_meminfo(meminfo);
-
-			std::thread::sleep(Duration::from_secs(60));
+	match std::env::args().nth(1).as_deref() {
+		Some("graph") => {
+			gatherer::make_mem_graph(&state);
 		}
-	});
+		_ => (),
+	}
+
+	let mut gatherer = Gatherer::new(state.clone());
+
+	#[cfg(target_os = "linux")]
+	gatherer.start();
+
+	#[cfg(not(target_os = "linux"))]
+	tracing::warn!("compilation target was not linux; not collecing stat data");
 
 	let settings = Settings {
 		template_dir: Utf8PathBuf::from(webroot.join(templates))
@@ -93,9 +112,11 @@ async fn main() {
 
 	let app = Router::new()
 		.route("/", get(index_handler))
+		.route("/api/stats/:name", get(stats))
 		.route("/*path", get(handler))
 		.layer(Extension(fs))
-		.layer(Extension(settings)).with_state(state);
+		.layer(Extension(settings))
+		.with_state(state);
 
 	let listener = tokio::net::TcpListener::bind("0.0.0.0:2560").await.unwrap();
 	axum::serve(listener, app).await.unwrap()
@@ -190,6 +211,7 @@ fn redirect<S: Into<String>>(redirection: S) -> Response {
 const STREAM_AFTER: u64 = 20 * 1024 * 1024;
 
 async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
+	tracing::debug!(target: "send_file", filepath = ?filepath);
 	let ext = filepath.extension().unwrap_or_default();
 	let stem = filepath.file_stem().unwrap_or_default();
 
@@ -209,6 +231,7 @@ async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
 		"png" => "image/png",
 		_ => "",
 	};
+	tracing::debug!(target: "send_file", mime = ?mime);
 
 	let mut response = Response::builder();
 	if !mime.is_empty() {
@@ -223,6 +246,8 @@ async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
 		let stream = ReaderStream::new(file);
 		Ok(response.body(Body::from_stream(stream)).unwrap())
 	} else {
+		tracing::debug!("small file, sending entirely at once");
+
 		let content = Filesystem::read(filepath).await?;
 		Ok(response.body(Body::from(content)).unwrap())
 	}
@@ -380,63 +405,33 @@ async fn send_template(
 
 fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: String) -> String {
 	let Ok(mut doc) = Document::from_str(&marked, Options::default()) else {
-		return marked
+		return marked;
 	};
 
 	if frontmatter.get("system-stats").is_some() {
 		let mem = state.database.get_last_host_meminfo();
 
-		doc.set("stats.mem.total", mem.total_kb / 1024);
-		doc.set("stats.mem.usage", mem.usage() / 1024);
+		doc.set("stats.mem.total", mem.total_kb / 1000);
+		doc.set("stats.mem.usage", mem.usage() / 1000);
 	}
 
 	doc.compile()
 }
 
-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);
+async fn stats(State(state): State<AwakeState>, Path(name): Path<String>) -> Response {
+	const NAMES: &[&'static str] = &["current_hostmeminfo.gif", "current_hostnetinfo.gif"];
 
-		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,
-					_ => ()
-				}
-			}
+	if NAMES.contains(&name.as_str()) {
+		let path = state.cache_path.join(name);
+		match send_file(path).await {
+			Ok(resp) => resp,
+			Err(e) => Response::builder().body(Body::from(e.to_string())).unwrap(),
 		}
+	} else {
+		tracing::debug!("invalid stat requested: {name}");
 
-		meminfo
-	}
-
-	pub fn usage(&self) -> usize {
-		self.total - self.avaialable
+		Response::builder()
+			.body(Body::from("that stat is not real"))
+			.unwrap()
 	}
-}
\ No newline at end of file
+}