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!("");
#[rustfmt::skip]
println!("
\n\
corgi stats\n\
\n\
");
println!("");
println!("Corgi Stats :)
");
#[rustfmt::skip]
println!("\n\
\n\
\t\n\
\t\tRequests for the last 15 minutes | \n\
\t
\n\
\t\n\
\t\t# Req. | \n\
\t\tReq/min | \n\
\t\tAgent | \n\
\t
\n\
\n");
for (count, agent) in &agents {
#[rustfmt::skip]
println!("\n\
\t{count} | \n\
\t{:.1} | \n\
\t{agent} | \n\
",
*count as f32 / 15.0);
}
println!("\n
");
#[rustfmt::skip]
println!("\n\
\n\
\t\n\
\t\tTop 10 User Agents All Time | \n\
\t
\n\
\t\n\
\t\t# Req. | \n\
\t\tReq/min | \n\
\t\tAgent | \n\
\t
\n\
\n");
// Finish what we started
println!("\n");
for (count, agent) in highest_five {
#[rustfmt::skip]
println!("\n\
\t{count} | \n\
\t{:.1} | \n\
\t{agent} | \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);
}