1use crate::response::IntoResponse;
2use crate::{Request, Response};
3use std::collections::HashMap;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::{Arc, RwLock};
7use std::time::{Duration, Instant};
8
9#[derive(Clone, Debug)]
11pub struct StatusConfig {
12 pub path: String,
14 pub title: String,
16}
17
18impl Default for StatusConfig {
19 fn default() -> Self {
20 Self {
21 path: "/status".to_string(),
22 title: "System Status".to_string(),
23 }
24 }
25}
26
27impl StatusConfig {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn path(mut self, path: impl Into<String>) -> Self {
33 self.path = path.into();
34 self
35 }
36
37 pub fn title(mut self, title: impl Into<String>) -> Self {
38 self.title = title.into();
39 self
40 }
41}
42
43#[derive(Debug, Clone, Default)]
45pub struct EndpointMetrics {
46 pub total_requests: u64,
47 pub successful_requests: u64,
48 pub failed_requests: u64,
49 pub total_latency_ms: u128,
50 pub last_access: Option<String>,
51}
52
53impl EndpointMetrics {
54 pub fn avg_latency_ms(&self) -> f64 {
55 if self.total_requests == 0 {
56 0.0
57 } else {
58 self.total_latency_ms as f64 / self.total_requests as f64
59 }
60 }
61
62 pub fn success_rate(&self) -> f64 {
63 if self.total_requests == 0 {
64 0.0
65 } else {
66 (self.successful_requests as f64 / self.total_requests as f64) * 100.0
67 }
68 }
69}
70
71#[derive(Debug, Default)]
73pub struct StatusMonitor {
74 metrics: RwLock<HashMap<String, EndpointMetrics>>,
76 start_time: Option<Instant>,
78}
79
80impl StatusMonitor {
81 pub fn new() -> Self {
82 Self {
83 metrics: RwLock::new(HashMap::new()),
84 start_time: Some(Instant::now()),
85 }
86 }
87
88 pub fn record_request(&self, path: &str, duration: Duration, success: bool) {
89 let mut metrics = self.metrics.write().unwrap();
90 let entry = metrics.entry(path.to_string()).or_default();
91
92 entry.total_requests += 1;
93 if success {
94 entry.successful_requests += 1;
95 } else {
96 entry.failed_requests += 1;
97 }
98 entry.total_latency_ms += duration.as_millis();
99
100 let now = chrono::Utc::now().to_rfc3339();
101 entry.last_access = Some(now);
102 }
103
104 pub fn get_uptime(&self) -> Duration {
105 self.start_time
106 .map(|t| t.elapsed())
107 .unwrap_or(Duration::from_secs(0))
108 }
109
110 pub fn get_snapshot(&self) -> HashMap<String, EndpointMetrics> {
111 self.metrics.read().unwrap().clone()
112 }
113}
114
115use crate::middleware::{BoxedNext, MiddlewareLayer};
116
117#[derive(Clone)]
119pub struct StatusLayer {
120 monitor: Arc<StatusMonitor>,
121}
122
123impl StatusLayer {
124 pub fn new(monitor: Arc<StatusMonitor>) -> Self {
125 Self { monitor }
126 }
127}
128
129impl MiddlewareLayer for StatusLayer {
130 fn call(
131 &self,
132 req: Request,
133 next: BoxedNext,
134 ) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>> {
135 let monitor = self.monitor.clone();
136 let path = req.uri().path().to_string();
137
138 Box::pin(async move {
139 let start = Instant::now();
140 let response = next(req).await;
141 let duration = start.elapsed();
142
143 let status = response.status();
144 let success = status.is_success() || status.is_redirection();
145
146 monitor.record_request(&path, duration, success);
147
148 response
149 })
150 }
151
152 fn clone_box(&self) -> Box<dyn MiddlewareLayer> {
153 Box::new(self.clone())
154 }
155}
156
157pub async fn status_handler(
159 monitor: Arc<StatusMonitor>,
160 config: StatusConfig,
161) -> impl IntoResponse {
162 let metrics = monitor.get_snapshot();
163 let uptime = monitor.get_uptime();
164
165 let mut html = format!(
167 r#"<!DOCTYPE html>
168<html>
169<head>
170 <title>{title}</title>
171 <style>
172 body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; background: #f0f2f5; color: #333; }}
173 .header {{ background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }}
174 .header h1 {{ margin: 0; color: #2c3e50; }}
175 .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; }}
176 .stat-card {{ background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
177 .stat-value {{ font-size: 24px; font-weight: bold; color: #3498db; }}
178 .stat-label {{ color: #7f8c8d; font-size: 14px; }}
179 table {{ width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
180 th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }}
181 th {{ background: #f8f9fa; font-weight: 600; color: #2c3e50; }}
182 tr:hover {{ background-color: #f5f5f5; }}
183 .status-ok {{ color: #27ae60; font-weight: bold; }}
184 .status-err {{ color: #e74c3c; font-weight: bold; }}
185 </style>
186 <meta http-equiv="refresh" content="5">
187</head>
188<body>
189 <div class="header">
190 <h1>{title}</h1>
191 <p>System Uptime: {uptime}</p>
192 </div>
193
194 <div class="stats-grid">
195 <div class="stat-card">
196 <div class="stat-value">{total_reqs}</div>
197 <div class="stat-label">Total Requests</div>
198 </div>
199 <div class="stat-card">
200 <div class="stat-value">{total_eps}</div>
201 <div class="stat-label">Active Endpoints</div>
202 </div>
203 </div>
204
205 <table>
206 <thead>
207 <tr>
208 <th>Endpoint</th>
209 <th>Requests</th>
210 <th>Success Rate</th>
211 <th>Avg Latency</th>
212 <th>Last Access</th>
213 </tr>
214 </thead>
215 <tbody>
216"#,
217 title = config.title,
218 uptime = format_duration(uptime),
219 total_reqs = metrics.values().map(|m| m.total_requests).sum::<u64>(),
220 total_eps = metrics.len()
221 );
222
223 let mut sorted_metrics: Vec<_> = metrics.iter().collect();
225 sorted_metrics.sort_by_key(|(k, _)| *k);
226
227 for (path, m) in sorted_metrics {
228 let success_class = if m.success_rate() > 95.0 {
229 "status-ok"
230 } else {
231 "status-err"
232 };
233
234 html.push_str(&format!(
235 r#" <tr>
236 <td><code>{}</code></td>
237 <td>{}</td>
238 <td class="{}">{:.1}%</td>
239 <td>{:.2} ms</td>
240 <td>{}</td>
241 </tr>
242"#,
243 path,
244 m.total_requests,
245 success_class,
246 m.success_rate(),
247 m.avg_latency_ms(),
248 m.last_access.as_deref().unwrap_or("-")
249 ));
250 }
251
252 html.push_str(
253 r#" </tbody>
254 </table>
255</body>
256</html>"#,
257 );
258
259 crate::response::Html(html)
260}
261
262fn format_duration(d: Duration) -> String {
263 let seconds = d.as_secs();
264 let days = seconds / 86400;
265 let hours = (seconds % 86400) / 3600;
266 let minutes = (seconds % 3600) / 60;
267 let secs = seconds % 60;
268
269 if days > 0 {
270 format!("{}d {}h {}m {}s", days, hours, minutes, secs)
271 } else if hours > 0 {
272 format!("{}h {}m {}s", hours, minutes, secs)
273 } else {
274 format!("{}m {}s", minutes, secs)
275 }
276}