1use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11#[derive(Default)]
12pub struct Metrics {
13 pub requests_total: AtomicU64,
14 pub status_2xx_total: AtomicU64,
15 pub status_3xx_total: AtomicU64,
16 pub status_4xx_total: AtomicU64,
17 pub status_5xx_total: AtomicU64,
18 pub upstream_connect_failures_total: AtomicU64,
19 pub upstream_timeouts_total: AtomicU64,
20 pub websocket_upgrades_total: AtomicU64,
21 pub host_rejected_total: AtomicU64,
22}
23
24impl Metrics {
25 pub fn new() -> Arc<Self> {
26 Arc::new(Self::default())
27 }
28
29 pub fn record_status(&self, status: u16) {
30 self.requests_total.fetch_add(1, Ordering::Relaxed);
31 let bucket = match status {
32 200..=299 => &self.status_2xx_total,
33 300..=399 => &self.status_3xx_total,
34 400..=499 => &self.status_4xx_total,
35 500..=599 => &self.status_5xx_total,
36 _ => return, };
38 bucket.fetch_add(1, Ordering::Relaxed);
39 }
40
41 pub fn render_prometheus(&self) -> String {
42 let mut out = String::with_capacity(1024);
43 emit_counter(&mut out, "fbi_proxy_requests_total",
44 "Total HTTP requests handled by fbi-proxy.",
45 self.requests_total.load(Ordering::Relaxed));
46 emit_counter(&mut out, "fbi_proxy_status_2xx_total",
47 "HTTP responses with status 2xx.",
48 self.status_2xx_total.load(Ordering::Relaxed));
49 emit_counter(&mut out, "fbi_proxy_status_3xx_total",
50 "HTTP responses with status 3xx.",
51 self.status_3xx_total.load(Ordering::Relaxed));
52 emit_counter(&mut out, "fbi_proxy_status_4xx_total",
53 "HTTP responses with status 4xx.",
54 self.status_4xx_total.load(Ordering::Relaxed));
55 emit_counter(&mut out, "fbi_proxy_status_5xx_total",
56 "HTTP responses with status 5xx.",
57 self.status_5xx_total.load(Ordering::Relaxed));
58 emit_counter(&mut out, "fbi_proxy_upstream_connect_failures_total",
59 "Failed TCP/TLS connects to upstream.",
60 self.upstream_connect_failures_total.load(Ordering::Relaxed));
61 emit_counter(&mut out, "fbi_proxy_upstream_timeouts_total",
62 "Upstream requests that exceeded the request timeout.",
63 self.upstream_timeouts_total.load(Ordering::Relaxed));
64 emit_counter(&mut out, "fbi_proxy_websocket_upgrades_total",
65 "WebSocket upgrade requests handled.",
66 self.websocket_upgrades_total.load(Ordering::Relaxed));
67 emit_counter(&mut out, "fbi_proxy_host_rejected_total",
68 "Requests rejected because the Host header didn't match the domain filter or any route.",
69 self.host_rejected_total.load(Ordering::Relaxed));
70 out
71 }
72}
73
74fn emit_counter(out: &mut String, name: &str, help: &str, value: u64) {
75 use std::fmt::Write;
76 let _ = writeln!(out, "# HELP {} {}", name, help);
77 let _ = writeln!(out, "# TYPE {} counter", name);
78 let _ = writeln!(out, "{} {}", name, value);
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn record_status_routes_to_correct_bucket() {
87 let m = Metrics::new();
88 m.record_status(200);
89 m.record_status(204);
90 m.record_status(302);
91 m.record_status(404);
92 m.record_status(502);
93 m.record_status(502);
94 assert_eq!(m.requests_total.load(Ordering::Relaxed), 6);
95 assert_eq!(m.status_2xx_total.load(Ordering::Relaxed), 2);
96 assert_eq!(m.status_3xx_total.load(Ordering::Relaxed), 1);
97 assert_eq!(m.status_4xx_total.load(Ordering::Relaxed), 1);
98 assert_eq!(m.status_5xx_total.load(Ordering::Relaxed), 2);
99 }
100
101 #[test]
102 fn render_includes_help_and_type_lines() {
103 let m = Metrics::new();
104 m.record_status(200);
105 let out = m.render_prometheus();
106 assert!(out.contains("# HELP fbi_proxy_requests_total"));
107 assert!(out.contains("# TYPE fbi_proxy_requests_total counter"));
108 assert!(out.contains("fbi_proxy_requests_total 1\n"));
109 assert!(out.contains("fbi_proxy_status_2xx_total 1\n"));
110 }
111}