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    /// Create a new metrics middleware instance.
35    pub fn new() -> Self {
36        Self
37    }
38}
39
40#[async_trait]
41impl Middleware for MetricsMiddleware {
42    async fn handle(&self, request: Request, next: Next) -> Response {
43        // Skip if metrics collection is disabled
44        if !metrics::is_enabled() {
45            return next(request).await;
46        }
47
48        // Skip internal debug endpoints
49        let path = request.path();
50        if path.starts_with("/_ferro/") {
51            return next(request).await;
52        }
53
54        let start = Instant::now();
55        let method = request.method().to_string();
56
57        // Get route pattern (with placeholders like {id}) instead of actual path
58        // This groups metrics by route pattern, not individual URLs
59        let route_pattern = request
60            .route_pattern()
61            .unwrap_or_else(|| "UNMATCHED".to_string());
62
63        // Execute the rest of the middleware chain and handler
64        let response = next(request).await;
65
66        let duration = start.elapsed();
67
68        // Determine if this is an error response
69        let is_error = match &response {
70            Ok(resp) => resp.status_code() >= 400,
71            Err(resp) => resp.status_code() >= 400,
72        };
73
74        // Record the metrics
75        metrics::record_request(&route_pattern, &method, duration, is_error);
76
77        response
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_metrics_middleware_new() {
87        let middleware = MetricsMiddleware::new();
88        assert!(format!("{middleware:?}").contains("MetricsMiddleware"));
89    }
90
91    #[test]
92    fn test_metrics_middleware_default() {
93        let middleware = MetricsMiddleware;
94        assert!(format!("{middleware:?}").contains("MetricsMiddleware"));
95    }
96
97    #[test]
98    fn test_metrics_middleware_clone() {
99        let middleware = MetricsMiddleware::new();
100        let cloned = middleware;
101        // Both should exist and be the same type
102        assert!(format!("{cloned:?}").contains("MetricsMiddleware"));
103    }
104
105    #[test]
106    fn test_metrics_middleware_copy() {
107        let middleware = MetricsMiddleware::new();
108        let copied: MetricsMiddleware = middleware; // Copy semantics
109        let _original = middleware; // Original still usable
110        assert!(format!("{copied:?}").contains("MetricsMiddleware"));
111    }
112
113    // Note: Full middleware behavior (request handling, timing, error detection)
114    // requires integration testing with actual Request/Response types.
115    // The core metrics recording logic is tested in the metrics module.
116}