1use axum::{
2 extract::State,
3 response::{Html, IntoResponse, Json},
4 routing::{get, post},
5 Router,
6};
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10use crate::{error::ConsoleError, state::ConsoleState};
11
12const MONITORING_HTML: &str = r#"<!DOCTYPE html>
14<html lang="pt-BR">
15<head>
16 <meta charset="UTF-8">
17 <meta name="viewport" content="width=device-width, initial-scale=1.0">
18 <title>Advanced Monitoring - AVL Console</title>
19 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
20 <style>
21 * { margin: 0; padding: 0; box-sizing: border-box; }
22 body {
23 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24 background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
25 min-height: 100vh;
26 padding: 20px;
27 }
28 .container {
29 max-width: 1800px;
30 margin: 0 auto;
31 }
32 h1 {
33 color: white;
34 margin-bottom: 10px;
35 font-size: 36px;
36 text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
37 }
38 .subtitle {
39 color: rgba(255,255,255,0.9);
40 margin-bottom: 30px;
41 font-size: 18px;
42 }
43 .metrics-grid {
44 display: grid;
45 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
46 gap: 20px;
47 margin-bottom: 20px;
48 }
49 .metric-card {
50 background: white;
51 border-radius: 16px;
52 padding: 25px;
53 box-shadow: 0 10px 30px rgba(0,0,0,0.2);
54 transition: transform 0.2s;
55 }
56 .metric-card:hover {
57 transform: translateY(-5px);
58 }
59 .metric-card.anomaly {
60 border: 3px solid #dc3545;
61 animation: pulse 2s infinite;
62 }
63 @keyframes pulse {
64 0%, 100% { box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
65 50% { box-shadow: 0 10px 40px rgba(220, 53, 69, 0.5); }
66 }
67 .metric-header {
68 display: flex;
69 justify-content: space-between;
70 align-items: center;
71 margin-bottom: 15px;
72 }
73 .metric-title {
74 font-size: 14px;
75 color: #6c757d;
76 text-transform: uppercase;
77 letter-spacing: 0.5px;
78 font-weight: 600;
79 }
80 .metric-badge {
81 padding: 4px 12px;
82 border-radius: 12px;
83 font-size: 11px;
84 font-weight: 600;
85 }
86 .badge-normal {
87 background: #d4edda;
88 color: #155724;
89 }
90 .badge-warning {
91 background: #fff3cd;
92 color: #856404;
93 }
94 .badge-critical {
95 background: #f8d7da;
96 color: #721c24;
97 }
98 .metric-value {
99 font-size: 42px;
100 font-weight: 700;
101 color: #2a5298;
102 margin-bottom: 10px;
103 }
104 .metric-change {
105 font-size: 14px;
106 display: flex;
107 align-items: center;
108 gap: 5px;
109 }
110 .change-up {
111 color: #28a745;
112 }
113 .change-down {
114 color: #dc3545;
115 }
116 .charts-section {
117 display: grid;
118 grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
119 gap: 20px;
120 margin-bottom: 20px;
121 }
122 .chart-card {
123 background: white;
124 border-radius: 16px;
125 padding: 25px;
126 box-shadow: 0 10px 30px rgba(0,0,0,0.2);
127 }
128 .chart-title {
129 font-size: 18px;
130 font-weight: 600;
131 color: #2a5298;
132 margin-bottom: 20px;
133 }
134 .alerts-section {
135 background: white;
136 border-radius: 16px;
137 padding: 25px;
138 box-shadow: 0 10px 30px rgba(0,0,0,0.2);
139 }
140 .alert-item {
141 display: flex;
142 align-items: start;
143 gap: 15px;
144 padding: 15px;
145 border-radius: 12px;
146 margin-bottom: 10px;
147 border: 2px solid #e9ecef;
148 transition: all 0.2s;
149 }
150 .alert-item:hover {
151 border-color: #2a5298;
152 background: #f8f9fa;
153 }
154 .alert-icon {
155 font-size: 32px;
156 flex-shrink: 0;
157 }
158 .alert-content {
159 flex: 1;
160 }
161 .alert-title {
162 font-weight: 600;
163 color: #495057;
164 margin-bottom: 5px;
165 }
166 .alert-description {
167 font-size: 14px;
168 color: #6c757d;
169 margin-bottom: 8px;
170 }
171 .alert-time {
172 font-size: 12px;
173 color: #adb5bd;
174 }
175 .alert-actions {
176 display: flex;
177 gap: 8px;
178 }
179 .btn-small {
180 padding: 6px 12px;
181 border: none;
182 border-radius: 6px;
183 font-size: 12px;
184 font-weight: 600;
185 cursor: pointer;
186 transition: all 0.2s;
187 }
188 .btn-resolve {
189 background: #28a745;
190 color: white;
191 }
192 .btn-resolve:hover {
193 background: #218838;
194 }
195 .btn-ignore {
196 background: #6c757d;
197 color: white;
198 }
199 .btn-ignore:hover {
200 background: #5a6268;
201 }
202 .ml-insights {
203 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
204 border-radius: 16px;
205 padding: 25px;
206 color: white;
207 margin-top: 20px;
208 box-shadow: 0 10px 30px rgba(0,0,0,0.2);
209 }
210 .insight-title {
211 font-size: 20px;
212 font-weight: 600;
213 margin-bottom: 15px;
214 display: flex;
215 align-items: center;
216 gap: 10px;
217 }
218 .insight-list {
219 display: flex;
220 flex-direction: column;
221 gap: 12px;
222 }
223 .insight-item {
224 background: rgba(255,255,255,0.1);
225 padding: 15px;
226 border-radius: 12px;
227 backdrop-filter: blur(10px);
228 }
229 .insight-item strong {
230 display: block;
231 margin-bottom: 5px;
232 }
233 .refresh-btn {
234 position: fixed;
235 bottom: 30px;
236 right: 30px;
237 width: 60px;
238 height: 60px;
239 border-radius: 50%;
240 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
241 color: white;
242 border: none;
243 font-size: 24px;
244 cursor: pointer;
245 box-shadow: 0 10px 30px rgba(102, 126, 234, 0.5);
246 transition: all 0.3s;
247 }
248 .refresh-btn:hover {
249 transform: scale(1.1) rotate(180deg);
250 }
251 </style>
252</head>
253<body>
254 <div class="container">
255 <h1>π¬ Advanced Monitoring & Alerts</h1>
256 <p class="subtitle">DetecΓ§Γ£o inteligente de anomalias com Machine Learning</p>
257
258 <div class="metrics-grid" id="metricsGrid">
259 <!-- Metrics will be populated here -->
260 </div>
261
262 <div class="charts-section">
263 <div class="chart-card">
264 <div class="chart-title">π Response Time Trend</div>
265 <canvas id="responseTimeChart"></canvas>
266 </div>
267 <div class="chart-card">
268 <div class="chart-title">πΎ Resource Usage</div>
269 <canvas id="resourceChart"></canvas>
270 </div>
271 </div>
272
273 <div class="alerts-section">
274 <h2 style="color: #2a5298; margin-bottom: 20px;">π¨ Active Alerts</h2>
275 <div id="alertsList">
276 <!-- Alerts will be populated here -->
277 </div>
278 </div>
279
280 <div class="ml-insights">
281 <div class="insight-title">
282 <span>π€</span>
283 <span>ML-Powered Insights</span>
284 </div>
285 <div class="insight-list" id="insightsList">
286 <!-- Insights will be populated here -->
287 </div>
288 </div>
289 </div>
290
291 <button class="refresh-btn" onclick="refreshData()">π</button>
292
293 <script>
294 let responseChart, resourceChart;
295
296 async function loadMetrics() {
297 try {
298 const response = await fetch('/monitoring/metrics');
299 const data = await response.json();
300 renderMetrics(data.metrics);
301 updateCharts(data.timeseries);
302 } catch (error) {
303 console.error('Failed to load metrics:', error);
304 }
305 }
306
307 async function loadAlerts() {
308 try {
309 const response = await fetch('/monitoring/alerts');
310 const data = await response.json();
311 renderAlerts(data.alerts);
312 } catch (error) {
313 console.error('Failed to load alerts:', error);
314 }
315 }
316
317 async function loadInsights() {
318 try {
319 const response = await fetch('/monitoring/insights');
320 const data = await response.json();
321 renderInsights(data.insights);
322 } catch (error) {
323 console.error('Failed to load insights:', error);
324 }
325 }
326
327 function renderMetrics(metrics) {
328 const grid = document.getElementById('metricsGrid');
329 grid.innerHTML = metrics.map(metric => {
330 const badgeClass = metric.status === 'normal' ? 'badge-normal' :
331 metric.status === 'warning' ? 'badge-warning' : 'badge-critical';
332 const changeClass = metric.change >= 0 ? 'change-up' : 'change-down';
333 const changeIcon = metric.change >= 0 ? 'β²' : 'βΌ';
334 const anomalyClass = metric.is_anomaly ? 'anomaly' : '';
335
336 return `
337 <div class="metric-card ${anomalyClass}">
338 <div class="metric-header">
339 <div class="metric-title">${metric.name}</div>
340 <div class="metric-badge ${badgeClass}">${metric.status.toUpperCase()}</div>
341 </div>
342 <div class="metric-value">${metric.value}${metric.unit}</div>
343 <div class="metric-change ${changeClass}">
344 ${changeIcon} ${Math.abs(metric.change)}% vs last hour
345 </div>
346 </div>
347 `;
348 }).join('');
349 }
350
351 function renderAlerts(alerts) {
352 const list = document.getElementById('alertsList');
353 if (alerts.length === 0) {
354 list.innerHTML = '<p style="text-align: center; color: #6c757d; padding: 40px;">β
No active alerts</p>';
355 return;
356 }
357
358 list.innerHTML = alerts.map(alert => `
359 <div class="alert-item">
360 <div class="alert-icon">${alert.icon}</div>
361 <div class="alert-content">
362 <div class="alert-title">${alert.title}</div>
363 <div class="alert-description">${alert.description}</div>
364 <div class="alert-time">π ${alert.time}</div>
365 </div>
366 <div class="alert-actions">
367 <button class="btn-small btn-resolve" onclick="resolveAlert('${alert.id}')">Resolve</button>
368 <button class="btn-small btn-ignore" onclick="ignoreAlert('${alert.id}')">Ignore</button>
369 </div>
370 </div>
371 `).join('');
372 }
373
374 function renderInsights(insights) {
375 const list = document.getElementById('insightsList');
376 list.innerHTML = insights.map(insight => `
377 <div class="insight-item">
378 <strong>${insight.title}</strong>
379 <div>${insight.description}</div>
380 </div>
381 `).join('');
382 }
383
384 function initCharts() {
385 const ctx1 = document.getElementById('responseTimeChart').getContext('2d');
386 responseChart = new Chart(ctx1, {
387 type: 'line',
388 data: {
389 labels: [],
390 datasets: [{
391 label: 'Response Time (ms)',
392 data: [],
393 borderColor: '#2a5298',
394 backgroundColor: 'rgba(42, 82, 152, 0.1)',
395 tension: 0.4,
396 fill: true
397 }]
398 },
399 options: {
400 responsive: true,
401 plugins: {
402 legend: { display: false }
403 },
404 scales: {
405 y: { beginAtZero: true }
406 }
407 }
408 });
409
410 const ctx2 = document.getElementById('resourceChart').getContext('2d');
411 resourceChart = new Chart(ctx2, {
412 type: 'bar',
413 data: {
414 labels: ['CPU', 'Memory', 'Disk', 'Network'],
415 datasets: [{
416 label: 'Usage %',
417 data: [0, 0, 0, 0],
418 backgroundColor: [
419 '#667eea',
420 '#764ba2',
421 '#f093fb',
422 '#4facfe'
423 ]
424 }]
425 },
426 options: {
427 responsive: true,
428 plugins: {
429 legend: { display: false }
430 },
431 scales: {
432 y: {
433 beginAtZero: true,
434 max: 100
435 }
436 }
437 }
438 });
439 }
440
441 function updateCharts(timeseries) {
442 if (!timeseries) return;
443
444 // Update response time chart
445 if (timeseries.response_time) {
446 responseChart.data.labels = timeseries.response_time.labels;
447 responseChart.data.datasets[0].data = timeseries.response_time.values;
448 responseChart.update();
449 }
450
451 // Update resource chart
452 if (timeseries.resources) {
453 resourceChart.data.datasets[0].data = timeseries.resources;
454 resourceChart.update();
455 }
456 }
457
458 async function resolveAlert(id) {
459 try {
460 await fetch(`/monitoring/alerts/${id}/resolve`, { method: 'POST' });
461 await loadAlerts();
462 } catch (error) {
463 console.error('Failed to resolve alert:', error);
464 }
465 }
466
467 async function ignoreAlert(id) {
468 try {
469 await fetch(`/monitoring/alerts/${id}/ignore`, { method: 'POST' });
470 await loadAlerts();
471 } catch (error) {
472 console.error('Failed to ignore alert:', error);
473 }
474 }
475
476 function refreshData() {
477 loadMetrics();
478 loadAlerts();
479 loadInsights();
480 }
481
482 // Initialize
483 initCharts();
484 refreshData();
485
486 // Auto-refresh every 30 seconds
487 setInterval(refreshData, 30000);
488 </script>
489</body>
490</html>"#;
491
492#[derive(Debug, Serialize, Deserialize, Clone)]
494pub struct Metric {
495 pub name: String,
496 pub value: f64,
497 pub unit: String,
498 pub status: String, pub change: f64, pub is_anomaly: bool,
501 pub threshold: f64,
502}
503
504#[derive(Debug, Serialize, Deserialize, Clone)]
506pub struct Alert {
507 pub id: String,
508 pub title: String,
509 pub description: String,
510 pub severity: String, pub icon: String,
512 pub time: String,
513 pub metric: String,
514 pub value: f64,
515}
516
517#[derive(Debug, Serialize, Deserialize, Clone)]
519pub struct MLInsight {
520 pub title: String,
521 pub description: String,
522 pub confidence: f64,
523 pub recommendation: String,
524}
525
526#[derive(Debug, Serialize, Deserialize)]
528struct TimeSeriesData {
529 labels: Vec<String>,
530 values: Vec<f64>,
531}
532
533#[derive(Debug, Serialize)]
535struct MetricsResponse {
536 metrics: Vec<Metric>,
537 timeseries: serde_json::Value,
538}
539
540#[derive(Debug, Serialize)]
542struct AlertsResponse {
543 alerts: Vec<Alert>,
544}
545
546#[derive(Debug, Serialize)]
548struct InsightsResponse {
549 insights: Vec<MLInsight>,
550}
551
552async fn monitoring_ui() -> impl IntoResponse {
554 Html(MONITORING_HTML)
555}
556
557async fn get_metrics(
559 State(state): State<Arc<ConsoleState>>,
560) -> Result<Json<MetricsResponse>, ConsoleError> {
561 let metrics = vec![
563 Metric {
564 name: "Response Time".to_string(),
565 value: 45.2,
566 unit: "ms".to_string(),
567 status: "normal".to_string(),
568 change: -5.3,
569 is_anomaly: false,
570 threshold: 100.0,
571 },
572 Metric {
573 name: "Requests/sec".to_string(),
574 value: 1234.0,
575 unit: "".to_string(),
576 status: "normal".to_string(),
577 change: 12.5,
578 is_anomaly: false,
579 threshold: 2000.0,
580 },
581 Metric {
582 name: "Error Rate".to_string(),
583 value: 0.8,
584 unit: "%".to_string(),
585 status: "warning".to_string(),
586 change: 45.2,
587 is_anomaly: true,
588 threshold: 1.0,
589 },
590 Metric {
591 name: "CPU Usage".to_string(),
592 value: 67.3,
593 unit: "%".to_string(),
594 status: "normal".to_string(),
595 change: 3.1,
596 is_anomaly: false,
597 threshold: 80.0,
598 },
599 Metric {
600 name: "Memory Usage".to_string(),
601 value: 82.5,
602 unit: "%".to_string(),
603 status: "warning".to_string(),
604 change: 15.7,
605 is_anomaly: true,
606 threshold: 85.0,
607 },
608 Metric {
609 name: "Active Connections".to_string(),
610 value: 456.0,
611 unit: "".to_string(),
612 status: "normal".to_string(),
613 change: -2.1,
614 is_anomaly: false,
615 threshold: 1000.0,
616 },
617 ];
618
619 let timeseries = serde_json::json!({
621 "response_time": {
622 "labels": ["10:00", "10:15", "10:30", "10:45", "11:00", "11:15", "11:30"],
623 "values": [42.1, 43.5, 45.2, 44.8, 46.1, 45.2, 45.2]
624 },
625 "resources": [67.3, 82.5, 45.2, 34.1]
626 });
627
628 Ok(Json(MetricsResponse {
629 metrics,
630 timeseries,
631 }))
632}
633
634async fn get_alerts(
636 State(state): State<Arc<ConsoleState>>,
637) -> Result<Json<AlertsResponse>, ConsoleError> {
638 let alerts = vec![
639 Alert {
640 id: "alert_1".to_string(),
641 title: "Anomaly Detected: Error Rate Spike".to_string(),
642 description: "Error rate increased by 45% in the last hour. ML model detected unusual pattern."
643 .to_string(),
644 severity: "warning".to_string(),
645 icon: "β οΈ".to_string(),
646 time: "2 minutes ago".to_string(),
647 metric: "error_rate".to_string(),
648 value: 0.8,
649 },
650 Alert {
651 id: "alert_2".to_string(),
652 title: "Memory Usage Approaching Limit".to_string(),
653 description: "Memory usage at 82.5%. Consider scaling up or optimizing memory-intensive operations."
654 .to_string(),
655 severity: "warning".to_string(),
656 icon: "πΎ".to_string(),
657 time: "15 minutes ago".to_string(),
658 metric: "memory_usage".to_string(),
659 value: 82.5,
660 },
661 Alert {
662 id: "alert_3".to_string(),
663 title: "Unusual Traffic Pattern Detected".to_string(),
664 description: "ML model identified abnormal request distribution. Possible DDoS or bot activity."
665 .to_string(),
666 severity: "critical".to_string(),
667 icon: "π¨".to_string(),
668 time: "5 minutes ago".to_string(),
669 metric: "traffic_pattern".to_string(),
670 value: 95.2,
671 },
672 ];
673
674 Ok(Json(AlertsResponse { alerts }))
675}
676
677async fn get_insights(
679 State(state): State<Arc<ConsoleState>>,
680) -> Result<Json<InsightsResponse>, ConsoleError> {
681 let insights = vec![
682 MLInsight {
683 title: "Predictive Scaling Recommendation".to_string(),
684 description: "Based on historical patterns, traffic is expected to increase by 35% in the next 2 hours. Consider auto-scaling now.".to_string(),
685 confidence: 0.89,
686 recommendation: "Enable auto-scaling with min 3, max 10 instances".to_string(),
687 },
688 MLInsight {
689 title: "Cost Optimization Opportunity".to_string(),
690 description: "Database queries show N+1 pattern. Implementing batch loading could reduce costs by ~40%.".to_string(),
691 confidence: 0.92,
692 recommendation: "Review database.rs query patterns and implement eager loading".to_string(),
693 },
694 MLInsight {
695 title: "Performance Bottleneck Identified".to_string(),
696 description: "ML analysis shows 78% of slow requests involve /api/search endpoint. Consider caching or indexing improvements.".to_string(),
697 confidence: 0.85,
698 recommendation: "Add Redis cache layer for search results with 5-minute TTL".to_string(),
699 },
700 ];
701
702 Ok(Json(InsightsResponse { insights }))
703}
704
705async fn resolve_alert(
707 State(state): State<Arc<ConsoleState>>,
708 axum::extract::Path(alert_id): axum::extract::Path<String>,
709) -> Result<Json<serde_json::Value>, ConsoleError> {
710 Ok(Json(serde_json::json!({
712 "success": true,
713 "message": format!("Alert {} resolved", alert_id)
714 })))
715}
716
717async fn ignore_alert(
719 State(state): State<Arc<ConsoleState>>,
720 axum::extract::Path(alert_id): axum::extract::Path<String>,
721) -> Result<Json<serde_json::Value>, ConsoleError> {
722 Ok(Json(serde_json::json!({
724 "success": true,
725 "message": format!("Alert {} ignored", alert_id)
726 })))
727}
728
729pub fn detect_anomaly(value: f64, historical_values: &[f64], threshold_std: f64) -> bool {
732 if historical_values.is_empty() {
733 return false;
734 }
735
736 let mean = historical_values.iter().sum::<f64>() / historical_values.len() as f64;
737 let variance = historical_values
738 .iter()
739 .map(|x| (x - mean).powi(2))
740 .sum::<f64>()
741 / historical_values.len() as f64;
742 let std_dev = variance.sqrt();
743
744 (value - mean).abs() > threshold_std * std_dev
745}
746
747pub fn router(state: Arc<ConsoleState>) -> Router {
749 Router::new()
750 .route("/", get(monitoring_ui))
751 .route("/metrics", get(get_metrics))
752 .route("/alerts", get(get_alerts))
753 .route("/insights", get(get_insights))
754 .route("/alerts/:id/resolve", post(resolve_alert))
755 .route("/alerts/:id/ignore", post(ignore_alert))
756 .with_state(state)
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
764 fn test_anomaly_detection_normal() {
765 let historical = vec![100.0, 102.0, 98.0, 101.0, 99.0];
766 assert!(!detect_anomaly(100.5, &historical, 2.0));
767 }
768
769 #[test]
770 fn test_anomaly_detection_spike() {
771 let historical = vec![100.0, 102.0, 98.0, 101.0, 99.0];
772 assert!(detect_anomaly(150.0, &historical, 2.0));
773 }
774
775 #[test]
776 fn test_anomaly_detection_empty() {
777 assert!(!detect_anomaly(100.0, &[], 2.0));
778 }
779}