elif_http/middleware/core/
timing.rs1use crate::{
6 middleware::v2::{Middleware, Next, NextFuture},
7 request::ElifRequest,
8};
9use log::{debug, warn};
10use std::time::Instant;
11
12#[derive(Debug)]
14pub struct TimingMiddleware {
15 add_header: bool,
17 slow_request_threshold_ms: u64,
19}
20
21impl TimingMiddleware {
22 pub fn new() -> Self {
24 Self {
25 add_header: true,
26 slow_request_threshold_ms: 1000, }
28 }
29
30 pub fn without_header(mut self) -> Self {
32 self.add_header = false;
33 self
34 }
35
36 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#[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 let start_time = Instant::now();
81
82 debug!(
83 "⏱️ Request timing started for {} {}",
84 request.method,
85 request.uri.path()
86 );
87
88 let mut response = next.run(request).await;
90
91 let duration = start_time.elapsed();
93 let duration_ms = duration.as_millis() as u64;
94
95 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 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
121pub 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 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
163 ElifResponse::ok().text("Success")
164 })
165 })
166 .await;
167
168 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 assert!(!response.has_header("x-response-time"));
192 }
193
194 #[test]
195 fn test_request_start_time() {
196 let start = RequestStartTime::new();
197
198 std::thread::sleep(std::time::Duration::from_millis(1000));
200
201 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}