about summary refs log tree commit diff
path: root/corgi_stats/src/main.rs
blob: 10976850820af406150a6b5391a8b9b98c9eddf8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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;";

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 mut body = String::new();

	let now = OffsetDateTime::now_utc();
	let fifteen_ago = now - Duration::minutes(15);

	let start = Instant::now();

	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);

	body.push_str("<p>In the last fifteen minutes:<br/><code><pre>");
	body.push_str("total | req/m | agent\n");
	for (count, agent) in &agents {
		body.push_str(&format!(
			"{count:<5} | {:<5.1} | {agent}\n",
			*count as f32 / 15.0
		));
	}
	body.push_str("</pre></code></p>");

	body.push_str("<p>Highest ten all time requesters:<br/><code><pre>");
	body.push_str("  total  | %of10 | agent\n");
	for (count, agent) in highest_five {
		body.push_str(&format!(
			" {count:<7} | {:<5.1} | {agent}\n",
			(count as f32 / sum_highest_five as f32) * 100.0
		));
	}
	body.push_str("</pre></code></p>");

	body.push_str(&format!("<p>took {}ms</p>", start.elapsed().as_millis()));

	println!("Status: 200\nContent-Type: text/html\n");
	println!("{body}");
}

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);
}