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
|
use std::{io::Write, time::Instant};
use rusqlite::{Connection, params};
use time::{Duration, OffsetDateTime};
const TOP_TEN_ALL_TIME: &str =
"SELECT request_count, agent FROM agents ORDER BY request_count DESC LIMIT 10;";
const LAST_FIFTEEN_MINUTES: &str = "\
SELECT count(ephemeral_requests.request_id) as request_count, agents.agent
FROM ephemeral_requests
INNER JOIN requests ON ephemeral_requests.request_id = requests.id
INNER JOIN agents ON requests.agent_id = agents.id
WHERE ephemeral_requests.timestamp > ?1
GROUP BY requests.agent_id;";
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 start = Instant::now();
let mut prepared = db.prepare(LAST_FIFTEEN_MINUTES).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)
}
|