use std::{
collections::BTreeMap,
fmt::Write,
sync::{Arc, Mutex},
time::Duration,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpMetricLabels {
pub method: String,
pub route: String,
pub status: u16,
}
impl HttpMetricLabels {
pub fn new(method: impl Into<String>, route: impl Into<String>, status: u16) -> Self {
Self {
method: method.into(),
route: route.into(),
status,
}
}
}
#[derive(Debug, Clone, Default)]
struct HttpMetricValue {
count: u64,
duration_seconds_sum: f64,
}
#[derive(Debug, Clone, Default)]
pub struct MetricsRegistry {
http_requests: Arc<Mutex<BTreeMap<HttpMetricLabels, HttpMetricValue>>>,
}
impl MetricsRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn record_http_request(&self, labels: HttpMetricLabels, duration: Duration) {
let mut metrics = self.http_requests.lock().expect("metrics mutex");
let entry = metrics.entry(labels).or_default();
entry.count += 1;
entry.duration_seconds_sum += duration.as_secs_f64();
}
pub fn render_prometheus(&self) -> String {
let metrics = self.http_requests.lock().expect("metrics mutex");
let mut output = String::new();
output.push_str("# HELP rs_zero_http_requests_total Total number of HTTP requests.\n");
output.push_str("# TYPE rs_zero_http_requests_total counter\n");
for (labels, value) in metrics.iter() {
let _ = writeln!(
output,
"rs_zero_http_requests_total{{method=\"{}\",route=\"{}\",status=\"{}\"}} {}",
escape_label(&labels.method),
escape_label(&labels.route),
labels.status,
value.count
);
}
output.push_str(
"# HELP rs_zero_http_request_duration_seconds_sum Sum of HTTP request durations.\n",
);
output.push_str("# TYPE rs_zero_http_request_duration_seconds_sum counter\n");
for (labels, value) in metrics.iter() {
let _ = writeln!(
output,
"rs_zero_http_request_duration_seconds_sum{{method=\"{}\",route=\"{}\",status=\"{}\"}} {:.6}",
escape_label(&labels.method),
escape_label(&labels.route),
labels.status,
value.duration_seconds_sum
);
}
output
}
}
fn escape_label(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{HttpMetricLabels, MetricsRegistry};
#[test]
fn metrics_render_prometheus_text() {
let registry = MetricsRegistry::new();
registry.record_http_request(
HttpMetricLabels::new("GET", "/users/:id", 200),
Duration::from_millis(20),
);
let text = registry.render_prometheus();
assert!(text.contains("rs_zero_http_requests_total"));
assert!(text.contains("route=\"/users/:id\""));
assert!(!text.contains("/users/42"));
}
}