diff options
author | gennyble <gen@nyble.dev> | 2025-04-04 04:42:24 -0500 |
---|---|---|
committer | gennyble <gen@nyble.dev> | 2025-04-04 04:42:24 -0500 |
commit | 6fdcda8781438644f80f8df1022a25014b2304a7 (patch) | |
tree | dbf2ebf58969a5c2f3dcc8983ae473004bdbfaac /stats/src | |
parent | a5d0dd1653b564a56a224fe374503cb63c511cc1 (diff) | |
download | corgi-6fdcda8781438644f80f8df1022a25014b2304a7.tar.gz corgi-6fdcda8781438644f80f8df1022a25014b2304a7.zip |
meow
Diffstat (limited to 'stats/src')
-rw-r--r-- | stats/src/main.rs | 132 | ||||
-rw-r--r-- | stats/src/style.css | 37 |
2 files changed, 169 insertions, 0 deletions
diff --git a/stats/src/main.rs b/stats/src/main.rs new file mode 100644 index 0000000..2e0c372 --- /dev/null +++ b/stats/src/main.rs @@ -0,0 +1,132 @@ +use rusqlite::{Connection, params}; +use std::time::Instant; +use time::{Duration, OffsetDateTime}; + +// Thank you, cat, for optimizing my query +const TOP_TEN_ALL_TIME: &str = "\ + SELECT reqs.cnt, agents.agent + FROM agents + JOIN ( + SELECT count(id) as cnt, agent_id + FROM requests + GROUP BY agent_id + ) reqs + ON reqs.agent_id=agents.id + ORDER BY reqs.cnt DESC LIMIT 10; +"; + +const STYLE: &'static str = include_str!("style.css"); + +fn main() { + let db_path = std::env::var("CORGI_STATS_DB").ok(); + let db = if let Some(path) = db_path { + if let Ok(db) = Connection::open(path) { + db + } else { + error_and_die(500, "failed to open database"); + } + } else { + error_and_die(500, "database key not set"); + }; + + 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()); + + let mut prepared = db.prepare(TOP_TEN_ALL_TIME).unwrap(); + let highest_five: Vec<(usize, String)> = prepared + .query_map(params![], |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap() + .map(|r| r.unwrap()) + .collect(); + let sum_highest_five = highest_five.iter().fold(0, |acc, (count, _)| acc + count); + + println!("Content-Type: text/html\n"); + println!("<html>"); + #[rustfmt::skip] + println!("<head>\n\ + <title>corgi stats</title>\n\ + <style>\n{STYLE}\n</style>\n\ + </head>"); + + println!("<body>"); + println!("<h1>Corgi Stats :)</h1>"); + + #[rustfmt::skip] + println!("<table>\n\ + <thead>\n\ + \t<tr>\n\ + \t\t<th scope='row' colspan='3' class='ttitle'>Requests for the last 15 minutes</th>\n\ + \t</tr>\n\ + \t<tr>\n\ + \t\t<th># Req.</th>\n\ + \t\t<th>Req/min</th>\n\ + \t\t<th>Agent</th>\n\ + \t</tr>\n\ + </thead>\n<tbody>"); + + for (count, agent) in &agents { + #[rustfmt::skip] + println!("<tr>\n\ + \t<td>{count}</td>\n\ + \t<td>{:.1}</td>\n\ + \t<td>{agent}</td>\n\ + </tr>", + *count as f32 / 15.0); + } + + println!("</tbody>\n</table>"); + + #[rustfmt::skip] + println!("<table>\n\ + <thead>\n\ + \t<tr>\n\ + \t\t<th scope='row' colspan='3' class='ttitle'>Top 10 User Agents All Time</th>\n\ + \t</tr>\n\ + \t<tr>\n\ + \t\t<th># Req.</th>\n\ + \t\t<th>Req/min</th>\n\ + \t\t<th>Agent</th>\n\ + \t</tr>\n\ + </thead>\n<tbody>"); + + // Finish what we started + println!("</body>\n</html>"); + + for (count, agent) in highest_five { + #[rustfmt::skip] + println!("<tr>\n\ + \t<td>{count}</td>\n\ + \t<td>{:.1}</td>\n\ + \t<td>{agent}</td>\n\ + </tr>", + (count as f32 / sum_highest_five as f32) * 100.0); + } +} + +fn error_and_die<S: Into<String>>(status: u16, msg: S) -> ! { + println!("Status: {status}"); + println!("Content-Type: text/html\n"); + println!("<html>"); + println!("\t<head><title>{status}</title></head>"); + println!("\t<body style='width: 20rem; padding: 0px; margin: 2rem;'>"); + println!("\t\t<h1>{status}</h1>"); + println!("\t\t<hr/>"); + println!("\t\t<p>{}</p>", msg.into()); + println!("\t</body>\n</html>"); + + std::process::exit(0); +} diff --git a/stats/src/style.css b/stats/src/style.css new file mode 100644 index 0000000..5b3995d --- /dev/null +++ b/stats/src/style.css @@ -0,0 +1,37 @@ +h1 { + font-family: sans-serif; +} + +table { + border-collapse: collapse; + border: 2px solid gray; + margin: 1rem 0px; +} + +tr { + background-color: white; +} + +tbody>tr:nth-of-type(odd) { + background-color: cornsilk; +} + +th, +td { + border: 1px solid darkslateblue; + padding: 2px 3px; +} + +thead th { + background-color: darksalmon; +} + +th { + text-align: left; + padding: 4px 6px; + white-space: nowrap; +} + +th.ttitle { + text-align: center; +} \ No newline at end of file |