avl_console/
monitoring.rs

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
12/// Advanced Monitoring Dashboard HTML
13const 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/// Metric data structure
493#[derive(Debug, Serialize, Deserialize, Clone)]
494pub struct Metric {
495    pub name: String,
496    pub value: f64,
497    pub unit: String,
498    pub status: String, // "normal", "warning", "critical"
499    pub change: f64,    // percentage change
500    pub is_anomaly: bool,
501    pub threshold: f64,
502}
503
504/// Alert data structure
505#[derive(Debug, Serialize, Deserialize, Clone)]
506pub struct Alert {
507    pub id: String,
508    pub title: String,
509    pub description: String,
510    pub severity: String, // "info", "warning", "critical"
511    pub icon: String,
512    pub time: String,
513    pub metric: String,
514    pub value: f64,
515}
516
517/// ML Insight data structure
518#[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/// Time series data
527#[derive(Debug, Serialize, Deserialize)]
528struct TimeSeriesData {
529    labels: Vec<String>,
530    values: Vec<f64>,
531}
532
533/// Metrics response
534#[derive(Debug, Serialize)]
535struct MetricsResponse {
536    metrics: Vec<Metric>,
537    timeseries: serde_json::Value,
538}
539
540/// Alerts response
541#[derive(Debug, Serialize)]
542struct AlertsResponse {
543    alerts: Vec<Alert>,
544}
545
546/// Insights response
547#[derive(Debug, Serialize)]
548struct InsightsResponse {
549    insights: Vec<MLInsight>,
550}
551
552/// Monitoring dashboard UI
553async fn monitoring_ui() -> impl IntoResponse {
554    Html(MONITORING_HTML)
555}
556
557/// Get current metrics with anomaly detection
558async fn get_metrics(
559    State(state): State<Arc<ConsoleState>>,
560) -> Result<Json<MetricsResponse>, ConsoleError> {
561    // Simulate metrics with ML anomaly detection
562    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    // Generate time series data
620    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
634/// Get active alerts
635async 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
677/// Get ML-powered insights
678async 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
705/// Resolve an alert
706async 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    // In production, update alert status in database
711    Ok(Json(serde_json::json!({
712        "success": true,
713        "message": format!("Alert {} resolved", alert_id)
714    })))
715}
716
717/// Ignore an alert
718async 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    // In production, update alert status in database
723    Ok(Json(serde_json::json!({
724        "success": true,
725        "message": format!("Alert {} ignored", alert_id)
726    })))
727}
728
729/// Anomaly detection using simple statistical methods
730/// In production, this would use proper ML models (e.g., Isolation Forest, LSTM)
731pub 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
747/// Create router for monitoring
748pub 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}