about summary refs log tree commit diff
path: root/stats/src
diff options
context:
space:
mode:
Diffstat (limited to 'stats/src')
-rw-r--r--stats/src/favicon.gifbin0 -> 138 bytes
-rw-r--r--stats/src/main.rs152
-rw-r--r--stats/src/style.css37
3 files changed, 189 insertions, 0 deletions
diff --git a/stats/src/favicon.gif b/stats/src/favicon.gif
new file mode 100644
index 0000000..371343b
--- /dev/null
+++ b/stats/src/favicon.gif
Binary files differdiff --git a/stats/src/main.rs b/stats/src/main.rs
new file mode 100644
index 0000000..7e3f922
--- /dev/null
+++ b/stats/src/main.rs
@@ -0,0 +1,152 @@
+use std::{io::Write, time::Instant};
+
+use rusqlite::{Connection, params};
+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");
+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 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 start = Instant::now();
+	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);
+	let elapsed = start.elapsed();
+
+	println!("Content-Type: text/html\n");
+	println!("<html>");
+	#[rustfmt::skip]
+	println!("<head>\n\
+		<title>corgi stats</title>\n\
+		<style>\n{STYLE}\n</style>\n\
+		<link rel='icon' type='image/gif' href='/stats/favicon.gif' />\n\
+	</head>");
+
+	println!("<body>");
+	println!("<h1>Corgi Stats :)</h1>");
+	println!("<p>generated in {}ms</p>", elapsed.as_millis());
+
+	#[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>% of 10</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