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, SystemTime, UNIX_EPOCH};
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        entry.last_access = Some(format_unix_timestamp());
101    }
102
103    pub fn get_uptime(&self) -> Duration {
104        self.start_time
105            .map(|t| t.elapsed())
106            .unwrap_or(Duration::from_secs(0))
107    }
108
109    pub fn get_snapshot(&self) -> HashMap<String, EndpointMetrics> {
110        self.metrics.read().unwrap().clone()
111    }
112}
113
114use crate::middleware::{BoxedNext, MiddlewareLayer};
115
116/// Middleware layer for status monitoring
117#[derive(Clone)]
118pub struct StatusLayer {
119    monitor: Arc<StatusMonitor>,
120}
121
122impl StatusLayer {
123    pub fn new(monitor: Arc<StatusMonitor>) -> Self {
124        Self { monitor }
125    }
126}
127
128impl MiddlewareLayer for StatusLayer {
129    fn call(
130        &self,
131        req: Request,
132        next: BoxedNext,
133    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>> {
134        let monitor = self.monitor.clone();
135        let path = req.uri().path().to_string();
136
137        Box::pin(async move {
138            let start = Instant::now();
139            let response = next(req).await;
140            let duration = start.elapsed();
141
142            let status = response.status();
143            let success = status.is_success() || status.is_redirection();
144
145            monitor.record_request(&path, duration, success);
146
147            response
148        })
149    }
150
151    fn clone_box(&self) -> Box<dyn MiddlewareLayer> {
152        Box::new(self.clone())
153    }
154}
155
156/// HTML Status Page Handler
157pub async fn status_handler(
158    monitor: Arc<StatusMonitor>,
159    config: StatusConfig,
160) -> impl IntoResponse {
161    let metrics = monitor.get_snapshot();
162    let uptime = monitor.get_uptime();
163
164    // Simple HTML template
165    let mut html = format!(
166        r#"<!DOCTYPE html>
167<html>
168<head>
169    <title>{title}</title>
170    <style>
171        body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; background: #f0f2f5; color: #333; }}
172        .header {{ background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }}
173        .header h1 {{ margin: 0; color: #2c3e50; }}
174        .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; }}
175        .stat-card {{ background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
176        .stat-value {{ font-size: 24px; font-weight: bold; color: #3498db; }}
177        .stat-label {{ color: #7f8c8d; font-size: 14px; }}
178        table {{ width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
179        th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; }}
180        th {{ background: #f8f9fa; font-weight: 600; color: #2c3e50; }}
181        tr:hover {{ background-color: #f5f5f5; }}
182        .status-ok {{ color: #27ae60; font-weight: bold; }}
183        .status-err {{ color: #e74c3c; font-weight: bold; }}
184    </style>
185    <meta http-equiv="refresh" content="5">
186</head>
187<body>
188    <div class="header">
189        <h1>{title}</h1>
190        <p>System Uptime: {uptime}</p>
191    </div>
192
193    <div class="stats-grid">
194        <div class="stat-card">
195            <div class="stat-value">{total_reqs}</div>
196            <div class="stat-label">Total Requests</div>
197        </div>
198        <div class="stat-card">
199            <div class="stat-value">{total_eps}</div>
200            <div class="stat-label">Active Endpoints</div>
201        </div>
202    </div>
203
204    <table>
205        <thead>
206            <tr>
207                <th>Endpoint</th>
208                <th>Requests</th>
209                <th>Success Rate</th>
210                <th>Avg Latency</th>
211                <th>Last Access</th>
212            </tr>
213        </thead>
214        <tbody>
215"#,
216        title = config.title,
217        uptime = format_duration(uptime),
218        total_reqs = metrics.values().map(|m| m.total_requests).sum::<u64>(),
219        total_eps = metrics.len()
220    );
221
222    // Sort metrics by path
223    let mut sorted_metrics: Vec<_> = metrics.iter().collect();
224    sorted_metrics.sort_by_key(|(k, _)| *k);
225
226    for (path, m) in sorted_metrics {
227        let success_class = if m.success_rate() > 95.0 {
228            "status-ok"
229        } else {
230            "status-err"
231        };
232
233        html.push_str(&format!(
234            r#"            <tr>
235                <td><code>{}</code></td>
236                <td>{}</td>
237                <td class="{}">{:.1}%</td>
238                <td>{:.2} ms</td>
239                <td>{}</td>
240            </tr>
241"#,
242            path,
243            m.total_requests,
244            success_class,
245            m.success_rate(),
246            m.avg_latency_ms(),
247            m.last_access.as_deref().unwrap_or("-")
248        ));
249    }
250
251    html.push_str(
252        r#"        </tbody>
253    </table>
254</body>
255</html>"#,
256    );
257
258    crate::response::Html(html)
259}
260
261fn format_duration(d: Duration) -> String {
262    let seconds = d.as_secs();
263    let days = seconds / 86400;
264    let hours = (seconds % 86400) / 3600;
265    let minutes = (seconds % 3600) / 60;
266    let secs = seconds % 60;
267
268    if days > 0 {
269        format!("{}d {}h {}m {}s", days, hours, minutes, secs)
270    } else if hours > 0 {
271        format!("{}h {}m {}s", hours, minutes, secs)
272    } else {
273        format!("{}m {}s", minutes, secs)
274    }
275}
276
277fn format_unix_timestamp() -> String {
278    let now = SystemTime::now()
279        .duration_since(UNIX_EPOCH)
280        .unwrap_or_else(|_| Duration::from_secs(0));
281    format!("unix:{}", now.as_secs())
282}