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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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)
}
|