Skip to main content

rustapi_core/
status.rs

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/// Configuration for the Status Page
10#[derive(Clone, Debug)]
11pub struct StatusConfig {
12    /// Path to serve the status page (default: "/status")
13    pub path: String,
14    /// Title of the status page
15    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/// Metrics for a specific endpoint
44#[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/// Shared state for monitoring
72#[derive(Debug, Default)]
73pub struct StatusMonitor {
74    /// Map of route path -> metrics
75    metrics: RwLock<HashMap<String, EndpointMetrics>>,
76    /// System start time
77    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/// Middleware layer for status monitoring
118#[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
157/// HTML Status Page Handler
158pub 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    // Simple HTML template
166    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    // Sort metrics by path
224    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}