elif_http/middleware/core/
timing.rs

1//! # Timing Middleware
2//!
3//! HTTP request timing middleware for performance monitoring.
4
5use crate::{
6    middleware::v2::{Middleware, Next, NextFuture},
7    request::ElifRequest,
8};
9use log::{debug, warn};
10use std::time::Instant;
11
12/// Request timing middleware that tracks request duration and adds timing headers
13#[derive(Debug)]
14pub struct TimingMiddleware {
15    /// Whether to add X-Response-Time header to responses
16    add_header: bool,
17    /// Warning threshold in milliseconds for slow requests
18    slow_request_threshold_ms: u64,
19}
20
21impl TimingMiddleware {
22    /// Create new timing middleware with default settings
23    pub fn new() -> Self {
24        Self {
25            add_header: true,
26            slow_request_threshold_ms: 1000, // 1 second
27        }
28    }
29
30    /// Disable adding timing header to responses
31    pub fn without_header(mut self) -> Self {
32        self.add_header = false;
33        self
34    }
35
36    /// Set slow request warning threshold in milliseconds
37    pub fn with_slow_threshold(mut self, threshold_ms: u64) -> Self {
38        self.slow_request_threshold_ms = threshold_ms;
39        self
40    }
41}
42
43impl Default for TimingMiddleware {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49/// Extension key for storing request start time
50#[derive(Clone, Copy)]
51pub struct RequestStartTime(Instant);
52
53impl Default for RequestStartTime {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl RequestStartTime {
60    pub fn new() -> Self {
61        Self(Instant::now())
62    }
63
64    pub fn elapsed(&self) -> std::time::Duration {
65        self.0.elapsed()
66    }
67
68    pub fn elapsed_ms(&self) -> u64 {
69        self.elapsed().as_millis() as u64
70    }
71}
72
73impl Middleware for TimingMiddleware {
74    fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
75        let add_header = self.add_header;
76        let slow_threshold = self.slow_request_threshold_ms;
77
78        Box::pin(async move {
79            // Store start time
80            let start_time = Instant::now();
81
82            debug!(
83                "⏱️  Request timing started for {} {}",
84                request.method,
85                request.uri.path()
86            );
87
88            // Continue to next middleware/handler
89            let mut response = next.run(request).await;
90
91            // Calculate duration
92            let duration = start_time.elapsed();
93            let duration_ms = duration.as_millis() as u64;
94
95            // Add timing header if enabled
96            if add_header {
97                if let Err(e) = response.add_header("X-Response-Time", duration_ms.to_string()) {
98                    warn!("Failed to add X-Response-Time header: {}", e);
99                }
100            }
101
102            // Check for slow requests and log warning
103            if duration_ms > slow_threshold {
104                warn!(
105                    "🐌 Slow request detected: {}ms (threshold: {}ms)",
106                    duration_ms, slow_threshold
107                );
108            } else {
109                debug!("⏱️  Request completed in {}ms", duration_ms);
110            }
111
112            response
113        })
114    }
115
116    fn name(&self) -> &'static str {
117        "TimingMiddleware"
118    }
119}
120
121/// Utility function to format duration for display
122pub fn format_duration(duration: std::time::Duration) -> String {
123    let total_ms = duration.as_millis();
124
125    if total_ms >= 1000 {
126        format!("{:.2}s", duration.as_secs_f64())
127    } else if total_ms >= 1 {
128        format!("{}ms", total_ms)
129    } else {
130        format!("{}μs", duration.as_micros())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::middleware::v2::MiddlewarePipelineV2;
138    use crate::request::{ElifMethod, ElifRequest};
139    use crate::response::headers::ElifHeaderMap;
140    use crate::response::{ElifResponse, ElifStatusCode};
141    use tokio::time::Duration;
142
143    #[test]
144    fn test_format_duration() {
145        assert_eq!(format_duration(Duration::from_micros(500)), "500μs");
146        assert_eq!(format_duration(Duration::from_millis(150)), "150ms");
147        assert_eq!(format_duration(Duration::from_millis(1500)), "1.50s");
148    }
149
150    #[tokio::test]
151    async fn test_timing_middleware_v2() {
152        let middleware = TimingMiddleware::new();
153        let pipeline = MiddlewarePipelineV2::new().add(middleware);
154
155        let headers = ElifHeaderMap::new();
156        let request = ElifRequest::new(ElifMethod::GET, "/api/test".parse().unwrap(), headers);
157
158        let response = pipeline
159            .execute(request, |_req| {
160                Box::pin(async move {
161                    // Add small delay to test timing
162                    tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
163                    ElifResponse::ok().text("Success")
164                })
165            })
166            .await;
167
168        // Should complete successfully and have the timing header
169        assert_eq!(response.status_code(), ElifStatusCode::OK);
170        assert!(response.has_header("x-response-time"));
171    }
172
173    #[tokio::test]
174    async fn test_timing_middleware_without_header() {
175        let middleware = TimingMiddleware::new().without_header();
176        let pipeline = MiddlewarePipelineV2::new().add(middleware);
177
178        let request = ElifRequest::new(
179            ElifMethod::GET,
180            "/api/test".parse().unwrap(),
181            ElifHeaderMap::new(),
182        );
183
184        let response = pipeline
185            .execute(request, |_req| {
186                Box::pin(async move { ElifResponse::ok().text("Success") })
187            })
188            .await;
189
190        // Should NOT have timing header
191        assert!(!response.has_header("x-response-time"));
192    }
193
194    #[test]
195    fn test_request_start_time() {
196        let start = RequestStartTime::new();
197
198        // Add a tiny delay to ensure some time passes
199        std::thread::sleep(std::time::Duration::from_millis(1000));
200
201        // Should have elapsed time
202        assert!(start.elapsed().as_nanos() > 0);
203        assert!(start.elapsed_ms() > 0);
204    }
205
206    #[test]
207    fn test_timing_middleware_builder() {
208        let middleware = TimingMiddleware::new()
209            .with_slow_threshold(500)
210            .without_header();
211
212        assert_eq!(middleware.slow_request_threshold_ms, 500);
213        assert!(!middleware.add_header);
214    }
215}