use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, Mutex};
pub static SERVER_METRICS: LazyLock<ServerMetrics> = LazyLock::new(ServerMetrics::default);
pub fn server_metrics() -> &'static ServerMetrics {
&SERVER_METRICS
}
#[derive(Default)]
pub struct ServerMetrics {
requests: Mutex<std::collections::BTreeMap<(&'static str, &'static str), u64>>,
response_bytes: AtomicU64,
deployments: AtomicU64,
activations: AtomicU64,
cert_renewals: AtomicU64,
}
impl ServerMetrics {
pub fn record_request(&self, status: u16, bytes: u64) {
self.response_bytes.fetch_add(bytes, Ordering::Relaxed);
let mut map = self.requests.lock().unwrap();
*map.entry((status_class(status), cache_result(status)))
.or_insert(0) += 1;
}
pub fn record_deployment(&self) {
self.deployments.fetch_add(1, Ordering::Relaxed);
}
pub fn record_activation(&self) {
self.activations.fetch_add(1, Ordering::Relaxed);
}
pub fn record_cert_renewal(&self) {
self.cert_renewals.fetch_add(1, Ordering::Relaxed);
}
pub fn render_prometheus(&self) -> String {
let mut out = String::new();
out.push_str(
"# HELP boatramp_http_requests_total HTTP requests by status class and cache result.\n\
# TYPE boatramp_http_requests_total counter\n",
);
for ((class, cache), value) in self.requests.lock().unwrap().iter() {
out.push_str(&format!(
"boatramp_http_requests_total{{status_class=\"{class}\",cache_result=\"{cache}\"}} {value}\n"
));
}
for (name, help, value) in [
(
"boatramp_http_response_bytes_total",
"Total response body bytes streamed.",
self.response_bytes.load(Ordering::Relaxed),
),
(
"boatramp_deployments_total",
"Deployment manifests created.",
self.deployments.load(Ordering::Relaxed),
),
(
"boatramp_activations_total",
"Deployment activations (live/alias pointer flips).",
self.activations.load(Ordering::Relaxed),
),
(
"boatramp_cert_renewals_total",
"TLS certificate issues/renewals.",
self.cert_renewals.load(Ordering::Relaxed),
),
] {
out.push_str(&format!(
"# HELP {name} {help}\n# TYPE {name} counter\n{name} {value}\n"
));
}
out
}
}
fn status_class(status: u16) -> &'static str {
match status / 100 {
1 => "1xx",
2 => "2xx",
3 => "3xx",
4 => "4xx",
5 => "5xx",
_ => "other",
}
}
pub fn cache_result(status: u16) -> &'static str {
match status {
304 => "not-modified",
206 => "partial",
200 => "full",
s if (300..400).contains(&s) => "redirect",
s if s >= 400 => "error",
_ => "-",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn records_and_renders_request_dimensions() {
let m = ServerMetrics::default();
m.record_request(200, 1024);
m.record_request(200, 512);
m.record_request(304, 0);
m.record_request(404, 48);
m.record_deployment();
m.record_activation();
m.record_cert_renewal();
let out = m.render_prometheus();
assert!(out.contains("# TYPE boatramp_http_requests_total counter"));
assert!(out.contains(
"boatramp_http_requests_total{status_class=\"2xx\",cache_result=\"full\"} 2"
));
assert!(out.contains(
"boatramp_http_requests_total{status_class=\"3xx\",cache_result=\"not-modified\"} 1"
));
assert!(out.contains(
"boatramp_http_requests_total{status_class=\"4xx\",cache_result=\"error\"} 1"
));
assert!(out.contains("boatramp_http_response_bytes_total 1584"));
assert!(out.contains("boatramp_deployments_total 1"));
assert!(out.contains("boatramp_activations_total 1"));
assert!(out.contains("boatramp_cert_renewals_total 1"));
}
#[test]
fn cache_result_classifies_status() {
assert_eq!(cache_result(200), "full");
assert_eq!(cache_result(206), "partial");
assert_eq!(cache_result(304), "not-modified");
assert_eq!(cache_result(301), "redirect");
assert_eq!(cache_result(500), "error");
}
}