use std::{io::Write, time::Instant}; use rusqlite::{Connection, params}; use time::{Duration, OffsetDateTime}; const TOP_TEN_ALL_TIME: &str = "SELECT request_count, agent FROM agents ORDER BY request_count DESC LIMIT 10;"; const LAST_FIFTEEN_MINUTES: &str = "\ SELECT count(ephemeral_requests.request_id) as request_count, agents.agent FROM ephemeral_requests INNER JOIN requests ON ephemeral_requests.request_id = requests.id INNER JOIN agents ON requests.agent_id = agents.id WHERE ephemeral_requests.timestamp > ?1 GROUP BY requests.agent_id;"; const STYLE: &'static str = include_str!("style.css"); const FAVICON: &'static [u8] = include_bytes!("favicon.gif"); fn main() { let Some(path) = std::env::var("PATH_INFO").ok() else { error_and_die(500, "no path provided"); }; match path.as_ref() { "/stats/favicon.gif" => { println!("Content-Type: image/png\n"); std::io::stdout().write_all(FAVICON).unwrap(); std::process::exit(1); } "/stats" | "/stats/" => (), _ => error_and_die(404, "not found"), } 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 start = Instant::now(); let mut prepared = db.prepare(LAST_FIFTEEN_MINUTES).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); let elapsed = start.elapsed(); println!("Content-Type: text/html\n"); println!(""); #[rustfmt::skip] println!("\n\ corgi stats\n\ \n\ \n\ "); println!(""); println!("

Corgi Stats :)

"); println!("

generated in {}ms

", elapsed.as_millis()); #[rustfmt::skip] println!("\n\ \n\ \t\n\ \t\t\n\ \t\n\ \t\n\ \t\t\n\ \t\t\n\ \t\t\n\ \t\n\ \n"); for (count, agent) in &agents { #[rustfmt::skip] println!("\n\ \t\n\ \t\n\ \t\n\ ", *count as f32 / 15.0); } println!("\n
Requests for the last 15 minutes
# Req.Req/minAgent
{count}{:.1}{agent}
"); #[rustfmt::skip] println!("\n\ \n\ \t\n\ \t\t\n\ \t\n\ \t\n\ \t\t\n\ \t\t\n\ \t\t\n\ \t\n\ \n"); // Finish what we started println!("\n"); for (count, agent) in highest_five { #[rustfmt::skip] println!("\n\ \t\n\ \t\n\ \t\n\ ", (count as f32 / sum_highest_five as f32) * 100.0); } } fn error_and_die>(status: u16, msg: S) -> ! { println!("Status: {status}"); println!("Content-Type: text/html\n"); println!(""); println!("\t{status}"); println!("\t"); println!("\t\t

{status}

"); println!("\t\t
"); println!("\t\t

{}

", msg.into()); println!("\t\n"); std::process::exit(0) }
Top 10 User Agents All Time
# Req.% of 10Agent
{count}{:.1}{agent}