boatramp_server/
srvmetrics.rs1use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{LazyLock, Mutex};
16
17pub static SERVER_METRICS: LazyLock<ServerMetrics> = LazyLock::new(ServerMetrics::default);
19
20pub fn server_metrics() -> &'static ServerMetrics {
22 &SERVER_METRICS
23}
24
25#[derive(Default)]
28pub struct ServerMetrics {
29 requests: Mutex<std::collections::BTreeMap<(&'static str, &'static str), u64>>,
30 response_bytes: AtomicU64,
31 deployments: AtomicU64,
32 activations: AtomicU64,
33 cert_renewals: AtomicU64,
34}
35
36impl ServerMetrics {
37 pub fn record_request(&self, status: u16, bytes: u64) {
40 self.response_bytes.fetch_add(bytes, Ordering::Relaxed);
41 let mut map = self.requests.lock().unwrap();
42 *map.entry((status_class(status), cache_result(status)))
43 .or_insert(0) += 1;
44 }
45
46 pub fn record_deployment(&self) {
48 self.deployments.fetch_add(1, Ordering::Relaxed);
49 }
50
51 pub fn record_activation(&self) {
53 self.activations.fetch_add(1, Ordering::Relaxed);
54 }
55
56 pub fn record_cert_renewal(&self) {
58 self.cert_renewals.fetch_add(1, Ordering::Relaxed);
59 }
60
61 pub fn render_prometheus(&self) -> String {
63 let mut out = String::new();
64 out.push_str(
65 "# HELP boatramp_http_requests_total HTTP requests by status class and cache result.\n\
66 # TYPE boatramp_http_requests_total counter\n",
67 );
68 for ((class, cache), value) in self.requests.lock().unwrap().iter() {
69 out.push_str(&format!(
70 "boatramp_http_requests_total{{status_class=\"{class}\",cache_result=\"{cache}\"}} {value}\n"
71 ));
72 }
73 for (name, help, value) in [
74 (
75 "boatramp_http_response_bytes_total",
76 "Total response body bytes streamed.",
77 self.response_bytes.load(Ordering::Relaxed),
78 ),
79 (
80 "boatramp_deployments_total",
81 "Deployment manifests created.",
82 self.deployments.load(Ordering::Relaxed),
83 ),
84 (
85 "boatramp_activations_total",
86 "Deployment activations (live/alias pointer flips).",
87 self.activations.load(Ordering::Relaxed),
88 ),
89 (
90 "boatramp_cert_renewals_total",
91 "TLS certificate issues/renewals.",
92 self.cert_renewals.load(Ordering::Relaxed),
93 ),
94 ] {
95 out.push_str(&format!(
96 "# HELP {name} {help}\n# TYPE {name} counter\n{name} {value}\n"
97 ));
98 }
99 out
100 }
101}
102
103fn status_class(status: u16) -> &'static str {
105 match status / 100 {
106 1 => "1xx",
107 2 => "2xx",
108 3 => "3xx",
109 4 => "4xx",
110 5 => "5xx",
111 _ => "other",
112 }
113}
114
115pub fn cache_result(status: u16) -> &'static str {
119 match status {
120 304 => "not-modified",
121 206 => "partial",
122 200 => "full",
123 s if (300..400).contains(&s) => "redirect",
124 s if s >= 400 => "error",
125 _ => "-",
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn records_and_renders_request_dimensions() {
135 let m = ServerMetrics::default();
136 m.record_request(200, 1024);
137 m.record_request(200, 512);
138 m.record_request(304, 0);
139 m.record_request(404, 48);
140 m.record_deployment();
141 m.record_activation();
142 m.record_cert_renewal();
143
144 let out = m.render_prometheus();
145 assert!(out.contains("# TYPE boatramp_http_requests_total counter"));
146 assert!(out.contains(
147 "boatramp_http_requests_total{status_class=\"2xx\",cache_result=\"full\"} 2"
148 ));
149 assert!(out.contains(
150 "boatramp_http_requests_total{status_class=\"3xx\",cache_result=\"not-modified\"} 1"
151 ));
152 assert!(out.contains(
153 "boatramp_http_requests_total{status_class=\"4xx\",cache_result=\"error\"} 1"
154 ));
155 assert!(out.contains("boatramp_http_response_bytes_total 1584"));
156 assert!(out.contains("boatramp_deployments_total 1"));
157 assert!(out.contains("boatramp_activations_total 1"));
158 assert!(out.contains("boatramp_cert_renewals_total 1"));
159 }
160
161 #[test]
162 fn cache_result_classifies_status() {
163 assert_eq!(cache_result(200), "full");
164 assert_eq!(cache_result(206), "partial");
165 assert_eq!(cache_result(304), "not-modified");
166 assert_eq!(cache_result(301), "redirect");
167 assert_eq!(cache_result(500), "error");
168 }
169}