about summary refs log tree commit diff
path: root/corgi_stats/src/main.rs
blob: 2e0c372c932e097f74c7f86cab00e42fc2e5cd79 (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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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);
}