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.
 
 
 
 
 
 

176 lines
5.3 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;
}
let normalized_segment = if is_uuid(segment) {
"{id}"
} else if is_hex_hash(segment) {
"{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 is a hex hash (32+ hex chars, typical for SHA256, MD5, etc)
fn is_hex_hash(s: &str) -> bool {
s.len() >= 32 && s.chars().all(|c| c.is_ascii_hexdigit())
}
/// 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_preserves_static_routes() {
assert_eq!(normalize_path("/api/accounts"), "/api/accounts");
assert_eq!(normalize_path("/api/sync"), "/api/sync");
assert_eq!(normalize_path("/icons"), "/icons");
}
#[test]
fn test_normalize_path_replaces_uuid() {
let uuid = "12345678-1234-5678-9012-123456789012";
assert_eq!(
normalize_path(&format!("/api/accounts/{uuid}")),
"/api/accounts/{id}"
);
assert_eq!(
normalize_path(&format!("/ciphers/{uuid}")),
"/ciphers/{id}"
);
}
#[test]
fn test_normalize_path_replaces_sha256_hash() {
// SHA256 hashes are 64 hex characters
let sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
assert_eq!(
normalize_path(&format!("/attachments/{sha256}")),
"/attachments/{hash}"
);
}
#[test]
fn test_normalize_path_does_not_replace_short_hex() {
// Only consider 32+ char hex strings as hashes
assert_eq!(normalize_path("/api/hex123"), "/api/hex123");
assert_eq!(normalize_path("/test/abc"), "/test/abc");
assert_eq!(normalize_path("/api/abcdef1234567890"), "/api/abcdef1234567890"); // 16 chars
assert_eq!(normalize_path("/files/0123456789abcdef"), "/files/0123456789abcdef"); // 16 chars
}
#[test]
fn test_normalize_path_replaces_numbers() {
assert_eq!(normalize_path("/api/organizations/123"), "/api/organizations/{number}");
assert_eq!(normalize_path("/users/456/profile"), "/users/{number}/profile");
}
#[test]
fn test_normalize_path_root() {
assert_eq!(normalize_path("/"), "/");
}
#[test]
fn test_normalize_path_empty_segments() {
assert_eq!(normalize_path("//api//accounts"), "/api/accounts");
}
#[test]
fn test_is_uuid_valid() {
assert!(is_uuid("12345678-1234-5678-9012-123456789012"));
assert!(is_uuid("00000000-0000-0000-0000-000000000000"));
assert!(is_uuid("ffffffff-ffff-ffff-ffff-ffffffffffff"));
}
#[test]
fn test_is_uuid_invalid_format() {
assert!(!is_uuid("not-a-uuid"));
assert!(!is_uuid("12345678123456781234567812345678"));
assert!(!is_uuid("123"));
assert!(!is_uuid(""));
assert!(!is_uuid("12345678-1234-5678-9012-12345678901")); // Too short
assert!(!is_uuid("12345678-1234-5678-9012-1234567890123")); // Too long
}
#[test]
fn test_is_uuid_invalid_characters() {
assert!(!is_uuid("12345678-1234-5678-9012-12345678901z"));
assert!(!is_uuid("g2345678-1234-5678-9012-123456789012"));
}
#[test]
fn test_is_uuid_invalid_dash_positions() {
assert!(!is_uuid("12345678-1234-56789012-123456789012"));
assert!(!is_uuid("12345678-1234-5678-90121-23456789012"));
}
}