From f0d38312633675625001180bc49cce8f90e7ed95 Mon Sep 17 00:00:00 2001 From: andersonid Date: Tue, 30 Sep 2025 17:41:39 -0300 Subject: [PATCH] Feature: Add real Prometheus metrics visualization for historical analysis --- app/api/routes.py | 59 +++++++++++ app/static/index.html | 234 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 292 insertions(+), 1 deletion(-) diff --git a/app/api/routes.py b/app/api/routes.py index a4e07c7..ca978c2 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -488,6 +488,65 @@ async def get_historical_validations( logger.error(f"Error getting historical validations: {e}") raise HTTPException(status_code=500, detail=str(e)) +@api_router.get("/workloads/{namespace}/{workload}/metrics") +async def get_workload_historical_metrics( + namespace: str, + workload: str, + time_range: str = "24h" +): + """Get historical metrics for a specific workload (deployment/daemonset)""" + try: + prometheus_client = PrometheusClient() + + # Get CPU and Memory usage metrics for the workload + cpu_usage = await prometheus_client.query_range( + f'rate(container_cpu_usage_seconds_total{{namespace="{namespace}",pod=~"{workload}-.*"}}[5m])', + time_range + ) + + memory_usage = await prometheus_client.query_range( + f'container_memory_working_set_bytes{{namespace="{namespace}",pod=~"{workload}-.*"}}', + time_range + ) + + # Get resource requests and limits + cpu_requests = await prometheus_client.query_range( + f'kube_pod_container_resource_requests{{namespace="{namespace}",pod=~"{workload}-.*",resource="cpu"}}', + time_range + ) + + memory_requests = await prometheus_client.query_range( + f'kube_pod_container_resource_requests{{namespace="{namespace}",pod=~"{workload}-.*",resource="memory"}}', + time_range + ) + + cpu_limits = await prometheus_client.query_range( + f'kube_pod_container_resource_limits{{namespace="{namespace}",pod=~"{workload}-.*",resource="cpu"}}', + time_range + ) + + memory_limits = await prometheus_client.query_range( + f'kube_pod_container_resource_limits{{namespace="{namespace}",pod=~"{workload}-.*",resource="memory"}}', + time_range + ) + + return { + "workload": workload, + "namespace": namespace, + "time_range": time_range, + "metrics": { + "cpu_usage": cpu_usage, + "memory_usage": memory_usage, + "cpu_requests": cpu_requests, + "memory_requests": memory_requests, + "cpu_limits": cpu_limits, + "memory_limits": memory_limits + } + } + except Exception as e: + logger.error(f"Error getting workload metrics for {namespace}/{workload}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + @api_router.get("/cluster/historical-summary") async def get_cluster_historical_summary( time_range: str = "24h" diff --git a/app/static/index.html b/app/static/index.html index 57398a4..0a26d45 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -383,6 +383,34 @@ margin-top: 20px; text-align: right; } + + .workload-selector { + margin-bottom: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + } + + .workload-selector select { + margin: 0 10px; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + } + + .chart-container { + margin: 20px 0; + padding: 15px; + border: 1px solid #ddd; + border-radius: 8px; + background: white; + } + + .chart-container canvas { + border: 1px solid #eee; + border-radius: 4px; + } /* Problem Summary Table */ .problem-summary { @@ -1334,7 +1362,211 @@ // Action functions - these are defined above in the main functions function showHistoricalAnalysis() { - showSection('historical-analysis'); + // Create modal for historical analysis with real Prometheus data + let modal = document.getElementById('historicalModal'); + if (!modal) { + modal = document.createElement('div'); + modal.id = 'historicalModal'; + modal.className = 'modal'; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + + // Add close functionality + modal.querySelector('.close').onclick = () => modal.style.display = 'none'; + modal.onclick = (e) => { + if (e.target === modal) modal.style.display = 'none'; + }; + } + + // Populate workload selector + populateWorkloadSelector(); + modal.style.display = 'block'; + } + + function populateWorkloadSelector() { + if (!currentData || !currentData.namespaces) return; + + const select = document.getElementById('workloadSelect'); + select.innerHTML = ''; + + currentData.namespaces.forEach(namespace => { + Object.values(namespace.pods || {}).forEach(pod => { + // Extract workload name from pod name (remove random suffix) + const workloadName = pod.pod_name.replace(/-\w{10}-\w{5}$/, ''); + const option = document.createElement('option'); + option.value = `${namespace.namespace}/${workloadName}`; + option.textContent = `${namespace.namespace}/${workloadName}`; + select.appendChild(option); + }); + }); + } + + async function loadWorkloadMetrics() { + const workloadSelect = document.getElementById('workloadSelect'); + const timeRangeSelect = document.getElementById('timeRangeSelect'); + const chartsDiv = document.getElementById('metricsCharts'); + + if (!workloadSelect.value) { + chartsDiv.style.display = 'none'; + return; + } + + const [namespace, workload] = workloadSelect.value.split('/'); + const timeRange = timeRangeSelect.value; + + try { + chartsDiv.style.display = 'block'; + chartsDiv.innerHTML = '

Loading metrics from Prometheus...

'; + + const response = await fetch(`/api/v1/workloads/${namespace}/${workload}/metrics?time_range=${timeRange}`); + const data = await response.json(); + + if (data.metrics) { + renderMetricsCharts(data.metrics, timeRange); + } else { + chartsDiv.innerHTML = '

No metrics data available for this workload.

'; + } + } catch (error) { + console.error('Error loading metrics:', error); + chartsDiv.innerHTML = '

Error loading metrics. Please try again.

'; + } + } + + function renderMetricsCharts(metrics, timeRange) { + const chartsDiv = document.getElementById('metricsCharts'); + chartsDiv.innerHTML = ` +
+

CPU Usage vs Requests/Limits (${timeRange})

+ +
+
+

Memory Usage vs Requests/Limits (${timeRange})

+ +
+ `; + + // Simple chart rendering (you can replace with Chart.js or similar) + renderSimpleChart('cpuChart', metrics.cpu_usage, metrics.cpu_requests, metrics.cpu_limits, 'CPU (cores)'); + renderSimpleChart('memoryChart', metrics.memory_usage, metrics.memory_requests, metrics.memory_limits, 'Memory (bytes)'); + } + + function renderSimpleChart(canvasId, usage, requests, limits, unit) { + const canvas = document.getElementById(canvasId); + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + + // Clear canvas + ctx.clearRect(0, 0, width, height); + + // Draw axes + ctx.strokeStyle = '#333'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(50, height - 50); + ctx.lineTo(width - 50, height - 50); + ctx.moveTo(50, 50); + ctx.lineTo(50, height - 50); + ctx.stroke(); + + // Draw usage line + if (usage && usage.length > 0) { + ctx.strokeStyle = '#007bff'; + ctx.lineWidth = 2; + ctx.beginPath(); + + usage.forEach((point, index) => { + const x = 50 + (index * (width - 100) / usage.length); + const y = height - 50 - (point[1] * (height - 100) / Math.max(...usage.map(p => p[1]))); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.stroke(); + } + + // Draw requests line + if (requests && requests.length > 0) { + ctx.strokeStyle = '#28a745'; + ctx.lineWidth = 1; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + + requests.forEach((point, index) => { + const x = 50 + (index * (width - 100) / requests.length); + const y = height - 50 - (point[1] * (height - 100) / Math.max(...requests.map(p => p[1]))); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.stroke(); + } + + // Draw limits line + if (limits && limits.length > 0) { + ctx.strokeStyle = '#dc3545'; + ctx.lineWidth = 1; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + + limits.forEach((point, index) => { + const x = 50 + (index * (width - 100) / limits.length); + const y = height - 50 - (point[1] * (height - 100) / Math.max(...limits.map(p => p[1]))); + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + ctx.stroke(); + } + + // Reset line dash + ctx.setLineDash([]); + + // Add labels + ctx.fillStyle = '#333'; + ctx.font = '12px Arial'; + ctx.fillText(unit, 10, height / 2); + ctx.fillText('Time', width / 2, height - 10); } function exportComplianceReport() {