You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
105 lines
3.0 KiB
105 lines
3.0 KiB
/// Metrics middleware for automatic HTTP request instrumentation
|
|
use rocket::{
|
|
fairing::{Fairing, Info, Kind},
|
|
Data, Request, Response,
|
|
};
|
|
use std::time::Instant;
|
|
|
|
pub struct MetricsFairing;
|
|
|
|
#[rocket::async_trait]
|
|
impl Fairing for MetricsFairing {
|
|
fn info(&self) -> Info {
|
|
Info {
|
|
name: "Metrics Collection",
|
|
kind: Kind::Request | Kind::Response,
|
|
}
|
|
}
|
|
|
|
async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
|
|
req.local_cache(|| RequestTimer {
|
|
start_time: Instant::now(),
|
|
});
|
|
}
|
|
|
|
async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
|
|
let timer = req.local_cache(|| RequestTimer {
|
|
start_time: Instant::now(),
|
|
});
|
|
let duration = timer.start_time.elapsed();
|
|
let method = req.method().as_str();
|
|
let path = normalize_path(req.uri().path().as_str());
|
|
let status = res.status().code;
|
|
|
|
// Record metrics
|
|
crate::metrics::increment_http_requests(method, &path, status);
|
|
crate::metrics::observe_http_request_duration(method, &path, duration.as_secs_f64());
|
|
}
|
|
}
|
|
|
|
struct RequestTimer {
|
|
start_time: Instant,
|
|
}
|
|
|
|
/// Normalize paths to avoid high cardinality metrics
|
|
/// Convert dynamic segments to static labels
|
|
fn normalize_path(path: &str) -> String {
|
|
let segments: Vec<&str> = path.split('/').collect();
|
|
let mut normalized = Vec::new();
|
|
|
|
for segment in segments {
|
|
if segment.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Common patterns in Vaultwarden routes
|
|
let normalized_segment = if is_uuid(segment) {
|
|
"{id}"
|
|
} else if segment.chars().all(|c| c.is_ascii_hexdigit()) && segment.len() > 10 {
|
|
"{hash}"
|
|
} else if segment.chars().all(|c| c.is_ascii_digit()) {
|
|
"{number}"
|
|
} else {
|
|
segment
|
|
};
|
|
|
|
normalized.push(normalized_segment);
|
|
}
|
|
|
|
if normalized.is_empty() {
|
|
"/".to_string()
|
|
} else {
|
|
format!("/{}", normalized.join("/"))
|
|
}
|
|
}
|
|
|
|
/// Check if a string looks like a UUID
|
|
fn is_uuid(s: &str) -> bool {
|
|
s.len() == 36
|
|
&& s.chars().enumerate().all(|(i, c)| match i {
|
|
8 | 13 | 18 | 23 => c == '-',
|
|
_ => c.is_ascii_hexdigit(),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_normalize_path() {
|
|
assert_eq!(normalize_path("/api/accounts"), "/api/accounts");
|
|
assert_eq!(normalize_path("/api/accounts/12345678-1234-5678-9012-123456789012"), "/api/accounts/{id}");
|
|
assert_eq!(normalize_path("/attachments/abc123def456"), "/attachments/{hash}");
|
|
assert_eq!(normalize_path("/api/organizations/123"), "/api/organizations/{number}");
|
|
assert_eq!(normalize_path("/"), "/");
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_uuid() {
|
|
assert!(is_uuid("12345678-1234-5678-9012-123456789012"));
|
|
assert!(!is_uuid("not-a-uuid"));
|
|
assert!(!is_uuid("12345678123456781234567812345678")); // No dashes
|
|
assert!(!is_uuid("123")); // Too short
|
|
}
|
|
}
|
|
|