Skip to main content

ferro_rs/middleware/
metrics.rs

1//! Metrics collection middleware
2//!
3//! Records request timing and error rates for performance monitoring.
4//! Should be registered as the first global middleware to capture full request duration.
5
6use crate::http::Request;
7use crate::http::Response;
8use crate::metrics;
9use crate::middleware::{Middleware, Next};
10use async_trait::async_trait;
11use std::time::Instant;
12
13/// Middleware that collects request metrics
14///
15/// Records:
16/// - Request count per route
17/// - Response time (min, max, avg)
18/// - Error count (4xx and 5xx responses)
19///
20/// # Example
21///
22/// ```rust,ignore
23/// use ferro_rs::middleware::MetricsMiddleware;
24///
25/// Server::from_config(router)
26///     .middleware(MetricsMiddleware)  // Add as first middleware
27///     .run()
28///     .await;
29/// ```
30#[derive(Debug, Clone, Copy, Default)]
31pub struct MetricsMiddleware;
32
33impl MetricsMiddleware {
34    pub fn new() -> Self {
35        Self
36    }
37}
38
39#[async_trait]
40impl Middleware for MetricsMiddleware {
41    async fn handle(&self, request: Request, next: Next) -> Response {
42        // Skip if metrics collection is disabled
43        if !metrics::is_enabled() {
44            return next(request).await;
45        }
46
47        // Skip internal debug endpoints
48        let path = request.path();
49        if path.starts_with("/_ferro/") {
50            return next(request).await;
51        }
52
53        let start = Instant::now();
54        let method = request.method().to_string();
55
56        // Get route pattern (with placeholders like {id}) instead of actual path
57        // This groups metrics by route pattern, not individual URLs
58        let route_pattern = request
59            .route_pattern()
60            .unwrap_or_else(|| "UNMATCHED".to_string());
61
62        // Execute the rest of the middleware chain and handler
63        let response = next(request).await;
64
65        let duration = start.elapsed();
66
67        // Determine if this is an error response
68        let is_error = match &response {
69            Ok(resp) => resp.status_code() >= 400,
70            Err(resp) => resp.status_code() >= 400,
71        };
72
73        // Record the metrics
74        metrics::record_request(&route_pattern, &method, duration, is_error);
75
76        response
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_metrics_middleware_new() {
86        let middleware = MetricsMiddleware::new();
87        assert!(format!("{middleware:?}").contains("MetricsMiddleware"));
88    }
89
90    #[test]
91    fn test_metrics_middleware_default() {
92        let middleware = MetricsMiddleware;
93        assert!(format!("{middleware:?}").contains("MetricsMiddleware"));
94    }
95
96    #[test]
97    fn test_metrics_middleware_clone() {
98        let middleware = MetricsMiddleware::new();
99        let cloned = middleware;
100        // Both should exist and be the same type
101        assert!(format!("{cloned:?}").contains("MetricsMiddleware"));
102    }
103
104    #[test]
105    fn test_metrics_middleware_copy() {
106        let middleware = MetricsMiddleware::new();
107        let copied: MetricsMiddleware = middleware; // Copy semantics
108        let _original = middleware; // Original still usable
109        assert!(format!("{copied:?}").contains("MetricsMiddleware"));
110    }
111
112    // Note: Full middleware behavior (request handling, timing, error detection)
113    // requires integration testing with actual Request/Response types.
114    // The core metrics recording logic is tested in the metrics module.
115}