From 1df2ee6bc406b87d96548da3a21e671490e82513 Mon Sep 17 00:00:00 2001 From: gennyble Date: Sat, 22 Mar 2025 16:11:20 -0500 Subject: stats! --- stats_module/Cargo.toml | 12 +++++ stats_module/src/lib.rs | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 stats_module/Cargo.toml create mode 100644 stats_module/src/lib.rs (limited to 'stats_module') diff --git a/stats_module/Cargo.toml b/stats_module/Cargo.toml new file mode 100644 index 0000000..ea0f965 --- /dev/null +++ b/stats_module/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "stats_module" +version = "0.1.0" +edition = "2024" + +[lib] +name = "stats" +crate-type = ["cdylib"] + +[dependencies] +rusqlite = { version = "0.34.0", features = ["bundled", "time"] } +time = "0.3.40" diff --git a/stats_module/src/lib.rs b/stats_module/src/lib.rs new file mode 100644 index 0000000..18c0e63 --- /dev/null +++ b/stats_module/src/lib.rs @@ -0,0 +1,118 @@ +use std::{ + ffi::{self, CStr, CString}, + sync::Mutex, +}; + +use rusqlite::{Connection, params}; +use time::{Duration, OffsetDateTime}; + +#[repr(C)] +struct ModuleRequest<'req> { + headers_len: ffi::c_ulong, + headers: &'req [[*const ffi::c_char; 2]], + body_len: ffi::c_ulong, + body: *const u8, +} + +#[repr(C)] +struct ModuleResponse { + status: ffi::c_ushort, + headers_len: ffi::c_ulong, + headers: &'static [[*const ffi::c_char; 2]], + body_len: ffi::c_ulong, + body: *const u8, +} + +const HEADERS: &'static [[*const ffi::c_char; 2]] = + &[[c"Content-Type".as_ptr(), c"text/html".as_ptr()]]; + +#[unsafe(no_mangle)] +extern "C" fn cgi_handle(req: *const ModuleRequest) -> *const ModuleResponse { + let mut ret = String::new(); + + // unwrap is bad here + let reqref = unsafe { req.as_ref() }.unwrap(); + + let mut stats_path = None; + for idx in 0..reqref.headers_len { + let kvarr = reqref.headers[idx as usize]; + let k = unsafe { CStr::from_ptr(kvarr[0]) }.to_string_lossy(); + let v = unsafe { CStr::from_ptr(kvarr[1]) }.to_string_lossy(); + + match k.as_ref() { + "CORGI_STATS_DB" => stats_path = Some(v.into_owned()), + _ => (), + } + } + + let db = if let Some(path) = stats_path { + Connection::open(path).unwrap() + } else { + return make_error(500, c"could not open stats database"); + }; + let now = OffsetDateTime::now_utc(); + let fifteen_ago = now - Duration::minutes(15); + + let query = "SELECT count(requests.id) AS request_count, agents.agent FROM requests \ + INNER JOIN agents ON requests.agent_id = agents.id \ + WHERE requests.timestamp > ?1 \ + GROUP BY requests.agent_id;"; + + let mut prepared = db.prepare(query).unwrap(); + let mut agents: Vec<(usize, String)> = prepared + .query_map(params![fifteen_ago], |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap() + .map(|r| r.unwrap()) + .collect(); + + agents.sort_by(|a, b| a.0.cmp(&b.0).reverse()); + + ret.push_str("

In the last fifteen minutes:

");
+	ret.push_str("total | req/m | agent\n");
+	for (count, agent) in &agents {
+		ret.push_str(&format!(
+			"{count:<5} | {:<5.1} | {agent}\n",
+			*count as f32 / 15.0
+		));
+	}
+	ret.push_str("

"); + + let body = CString::new(ret).unwrap(); + + let resp = ModuleResponse { + status: 200, + headers_len: 1, + headers: HEADERS, + body_len: body.as_bytes().len() as u64, + body: body.into_raw() as *const u8, + }; + + let boxed = Box::new(resp); + Box::::into_raw(boxed) +} + +fn make_error>(code: u16, msg: S) -> *const ModuleResponse { + let body = msg.into(); + + let resp = ModuleResponse { + status: code, + headers_len: 1, + headers: HEADERS, + body_len: body.as_bytes().len() as u64, + body: body.into_raw() as *const u8, + }; + + let boxed = Box::new(resp); + Box::::into_raw(boxed) +} + +#[unsafe(no_mangle)] +extern "C" fn cgi_cleanup(req: *const ModuleResponse) { + // from_raw what we need to here so that these get dropped + let boxed = unsafe { Box::from_raw(req as *mut ModuleResponse) }; + let body = unsafe { CString::from_raw(boxed.body as *mut i8) }; + + // Explicitly calling drop here to feel good about myself + drop(body); + drop(boxed); +} -- cgit 1.4.1-3-g733a5