Feature: Add real Prometheus metrics visualization for historical analysis

This commit is contained in:
2025-09-30 17:41:39 -03:00
parent d683704593
commit f0d3831263
2 changed files with 292 additions and 1 deletions

View File

@@ -488,6 +488,65 @@ async def get_historical_validations(
logger.error(f"Error getting historical validations: {e}") logger.error(f"Error getting historical validations: {e}")
raise HTTPException(status_code=500, detail=str(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") @api_router.get("/cluster/historical-summary")
async def get_cluster_historical_summary( async def get_cluster_historical_summary(
time_range: str = "24h" time_range: str = "24h"

View File

@@ -383,6 +383,34 @@
margin-top: 20px; margin-top: 20px;
text-align: right; 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 Table */
.problem-summary { .problem-summary {
@@ -1334,7 +1362,211 @@
// Action functions - these are defined above in the main functions // Action functions - these are defined above in the main functions
function showHistoricalAnalysis() { 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 = `
<div class="modal-content" style="width: 90%; max-width: 1200px;">
<div class="modal-header">
<h2>📈 Historical Analysis - Real Prometheus Metrics</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="historicalModalBody">
<div class="workload-selector">
<h3>Select Workload to Analyze:</h3>
<select id="workloadSelect" onchange="loadWorkloadMetrics()">
<option value="">Choose a workload...</option>
</select>
<select id="timeRangeSelect" onchange="loadWorkloadMetrics()">
<option value="1h">Last 1 hour</option>
<option value="6h">Last 6 hours</option>
<option value="24h" selected>Last 24 hours</option>
<option value="7d">Last 7 days</option>
</select>
</div>
<div id="metricsCharts" style="display: none;">
<div class="chart-container">
<h3>CPU Usage vs Requests/Limits</h3>
<canvas id="cpuChart" width="800" height="300"></canvas>
</div>
<div class="chart-container">
<h3>Memory Usage vs Requests/Limits</h3>
<canvas id="memoryChart" width="800" height="300"></canvas>
</div>
</div>
</div>
</div>
`;
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 = '<option value="">Choose a workload...</option>';
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 = '<p>Loading metrics from Prometheus...</p>';
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 = '<p>No metrics data available for this workload.</p>';
}
} catch (error) {
console.error('Error loading metrics:', error);
chartsDiv.innerHTML = '<p>Error loading metrics. Please try again.</p>';
}
}
function renderMetricsCharts(metrics, timeRange) {
const chartsDiv = document.getElementById('metricsCharts');
chartsDiv.innerHTML = `
<div class="chart-container">
<h3>CPU Usage vs Requests/Limits (${timeRange})</h3>
<canvas id="cpuChart" width="800" height="300"></canvas>
</div>
<div class="chart-container">
<h3>Memory Usage vs Requests/Limits (${timeRange})</h3>
<canvas id="memoryChart" width="800" height="300"></canvas>
</div>
`;
// 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() { function exportComplianceReport() {