Implement individual namespace historical analysis with modal UI
This commit is contained in:
@@ -423,6 +423,32 @@ async def get_cluster_historical_summary(
|
|||||||
logger.error(f"Error getting historical summary: {e}")
|
logger.error(f"Error getting historical summary: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@api_router.get("/namespace/{namespace}/historical-analysis")
|
||||||
|
async def get_namespace_historical_analysis(
|
||||||
|
namespace: str,
|
||||||
|
time_range: str = "24h",
|
||||||
|
prometheus_client=Depends(get_prometheus_client)
|
||||||
|
):
|
||||||
|
"""Get historical analysis for a specific namespace"""
|
||||||
|
try:
|
||||||
|
historical_service = HistoricalAnalysisService()
|
||||||
|
|
||||||
|
# Get historical analysis for the namespace
|
||||||
|
analysis = await historical_service.get_namespace_historical_analysis(
|
||||||
|
namespace, time_range, prometheus_client
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"namespace": namespace,
|
||||||
|
"time_range": time_range,
|
||||||
|
"analysis": analysis,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting historical analysis for namespace {namespace}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@api_router.get("/health")
|
@api_router.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""API health check"""
|
"""API health check"""
|
||||||
|
|||||||
@@ -443,3 +443,129 @@ class HistoricalAnalysisService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting historical summary: {e}")
|
logger.error(f"Error getting historical summary: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
async def get_namespace_historical_analysis(self, namespace: str, time_range: str, prometheus_client):
|
||||||
|
"""Get historical analysis for a specific namespace"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Getting historical analysis for namespace: {namespace}")
|
||||||
|
|
||||||
|
# Query for CPU usage by namespace
|
||||||
|
cpu_query = f'''
|
||||||
|
sum(rate(container_cpu_usage_seconds_total{{
|
||||||
|
namespace="{namespace}",
|
||||||
|
container!="POD",
|
||||||
|
container!=""
|
||||||
|
}}[{time_range}]))
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Query for memory usage by namespace
|
||||||
|
memory_query = f'''
|
||||||
|
sum(container_memory_working_set_bytes{{
|
||||||
|
namespace="{namespace}",
|
||||||
|
container!="POD",
|
||||||
|
container!=""
|
||||||
|
}})
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Query for CPU requests by namespace
|
||||||
|
cpu_requests_query = f'''
|
||||||
|
sum(kube_pod_container_resource_requests{{
|
||||||
|
namespace="{namespace}",
|
||||||
|
resource="cpu"
|
||||||
|
}})
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Query for memory requests by namespace
|
||||||
|
memory_requests_query = f'''
|
||||||
|
sum(kube_pod_container_resource_requests{{
|
||||||
|
namespace="{namespace}",
|
||||||
|
resource="memory"
|
||||||
|
}})
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Query for pod count by namespace
|
||||||
|
pod_count_query = f'''
|
||||||
|
count(kube_pod_info{{namespace="{namespace}"}})
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Execute queries
|
||||||
|
cpu_usage = await self._query_prometheus(cpu_query,
|
||||||
|
datetime.now() - timedelta(seconds=self.time_ranges[time_range]),
|
||||||
|
datetime.now(), prometheus_client)
|
||||||
|
memory_usage = await self._query_prometheus(memory_query,
|
||||||
|
datetime.now() - timedelta(seconds=self.time_ranges[time_range]),
|
||||||
|
datetime.now(), prometheus_client)
|
||||||
|
cpu_requests = await self._query_prometheus(cpu_requests_query,
|
||||||
|
datetime.now() - timedelta(seconds=self.time_ranges[time_range]),
|
||||||
|
datetime.now(), prometheus_client)
|
||||||
|
memory_requests = await self._query_prometheus(memory_requests_query,
|
||||||
|
datetime.now() - timedelta(seconds=self.time_ranges[time_range]),
|
||||||
|
datetime.now(), prometheus_client)
|
||||||
|
pod_count = await self._query_prometheus(pod_count_query,
|
||||||
|
datetime.now() - timedelta(seconds=self.time_ranges[time_range]),
|
||||||
|
datetime.now(), prometheus_client)
|
||||||
|
|
||||||
|
# Calculate utilization percentages
|
||||||
|
cpu_utilization = 0
|
||||||
|
memory_utilization = 0
|
||||||
|
|
||||||
|
if cpu_usage and cpu_requests and cpu_requests[0][1] != '0':
|
||||||
|
cpu_utilization = (float(cpu_usage[0][1]) / float(cpu_requests[0][1])) * 100
|
||||||
|
|
||||||
|
if memory_usage and memory_requests and memory_requests[0][1] != '0':
|
||||||
|
memory_utilization = (float(memory_usage[0][1]) / float(memory_requests[0][1])) * 100
|
||||||
|
|
||||||
|
# Generate recommendations based on utilization
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if cpu_utilization > 80:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "cpu_high_utilization",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": f"High CPU utilization: {cpu_utilization:.1f}%",
|
||||||
|
"recommendation": "Consider increasing CPU requests or optimizing application performance"
|
||||||
|
})
|
||||||
|
elif cpu_utilization < 20:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "cpu_low_utilization",
|
||||||
|
"severity": "info",
|
||||||
|
"message": f"Low CPU utilization: {cpu_utilization:.1f}%",
|
||||||
|
"recommendation": "Consider reducing CPU requests to optimize resource allocation"
|
||||||
|
})
|
||||||
|
|
||||||
|
if memory_utilization > 80:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "memory_high_utilization",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": f"High memory utilization: {memory_utilization:.1f}%",
|
||||||
|
"recommendation": "Consider increasing memory requests or optimizing memory usage"
|
||||||
|
})
|
||||||
|
elif memory_utilization < 20:
|
||||||
|
recommendations.append({
|
||||||
|
"type": "memory_low_utilization",
|
||||||
|
"severity": "info",
|
||||||
|
"message": f"Low memory utilization: {memory_utilization:.1f}%",
|
||||||
|
"recommendation": "Consider reducing memory requests to optimize resource allocation"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'namespace': namespace,
|
||||||
|
'time_range': time_range,
|
||||||
|
'cpu_usage': float(cpu_usage[0][1]) if cpu_usage else 0,
|
||||||
|
'memory_usage': float(memory_usage[0][1]) if memory_usage else 0,
|
||||||
|
'cpu_requests': float(cpu_requests[0][1]) if cpu_requests else 0,
|
||||||
|
'memory_requests': float(memory_requests[0][1]) if memory_requests else 0,
|
||||||
|
'cpu_utilization': cpu_utilization,
|
||||||
|
'memory_utilization': memory_utilization,
|
||||||
|
'pod_count': int(pod_count[0][1]) if pod_count else 0,
|
||||||
|
'recommendations': recommendations
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting historical analysis for namespace {namespace}: {e}")
|
||||||
|
return {
|
||||||
|
'namespace': namespace,
|
||||||
|
'time_range': time_range,
|
||||||
|
'error': str(e),
|
||||||
|
'recommendations': []
|
||||||
|
}
|
||||||
|
|||||||
@@ -497,6 +497,66 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #cc0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Filters */
|
/* Filters */
|
||||||
.filters {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -602,7 +662,6 @@
|
|||||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||||
<button class="btn" onclick="loadClusterStatus()">Refresh Status</button>
|
<button class="btn" onclick="loadClusterStatus()">Refresh Status</button>
|
||||||
<button class="btn btn-secondary" onclick="loadValidationsByNamespace()">View Analysis</button>
|
<button class="btn btn-secondary" onclick="loadValidationsByNamespace()">View Analysis</button>
|
||||||
<button class="btn btn-secondary" onclick="loadHistoricalValidations()">Historical Analysis</button>
|
|
||||||
<button class="btn btn-secondary" onclick="loadVPARecommendations()">View VPA</button>
|
<button class="btn btn-secondary" onclick="loadVPARecommendations()">View VPA</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -713,6 +772,19 @@
|
|||||||
|
|
||||||
<!-- Success -->
|
<!-- Success -->
|
||||||
<div class="success hidden" id="success"></div>
|
<div class="success hidden" id="success"></div>
|
||||||
|
|
||||||
|
<!-- Historical Analysis Modal -->
|
||||||
|
<div class="modal hidden" id="historicalModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="historicalModalTitle">Historical Analysis</h2>
|
||||||
|
<button class="modal-close" onclick="closeHistoricalModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="historicalModalBody">
|
||||||
|
<!-- Content will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -966,6 +1038,7 @@
|
|||||||
<div class="accordion-stat">${totalValidations} analysis</div>
|
<div class="accordion-stat">${totalValidations} analysis</div>
|
||||||
<div class="accordion-stat" style="color: #dc3545;">${errorCount} errors</div>
|
<div class="accordion-stat" style="color: #dc3545;">${errorCount} errors</div>
|
||||||
<div class="accordion-stat" style="color: #ffc107;">${warningCount} warnings</div>
|
<div class="accordion-stat" style="color: #ffc107;">${warningCount} warnings</div>
|
||||||
|
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; margin-left: 0.5rem;" onclick="event.stopPropagation(); loadNamespaceHistoricalAnalysis('${namespace.namespace}')">Historical Analysis</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-arrow">▶</div>
|
<div class="accordion-arrow">▶</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1277,6 +1350,122 @@
|
|||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Historical Analysis Modal Functions
|
||||||
|
async function loadNamespaceHistoricalAnalysis(namespace) {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timeRange = '24h'; // Default time range
|
||||||
|
const response = await fetch(`/api/v1/namespace/${namespace}/historical-analysis?time_range=${timeRange}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
displayNamespaceHistoricalAnalysis(data);
|
||||||
|
showHistoricalModal(namespace);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showError('Error loading historical analysis: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayNamespaceHistoricalAnalysis(data) {
|
||||||
|
const container = document.getElementById('historicalModalBody');
|
||||||
|
|
||||||
|
if (data.analysis.error) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<h3>Error loading historical data</h3>
|
||||||
|
<p>${data.analysis.error}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysis = data.analysis;
|
||||||
|
const recommendations = analysis.recommendations || [];
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="historical-summary">
|
||||||
|
<h3>Namespace: ${analysis.namespace}</h3>
|
||||||
|
<p><strong>Time Range:</strong> ${analysis.time_range}</p>
|
||||||
|
<p><strong>Pods:</strong> ${analysis.pod_count}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="historical-stats">
|
||||||
|
<div class="historical-stat">
|
||||||
|
<h4>CPU Usage</h4>
|
||||||
|
<div class="value">${analysis.cpu_usage.toFixed(3)} cores</div>
|
||||||
|
</div>
|
||||||
|
<div class="historical-stat">
|
||||||
|
<h4>Memory Usage</h4>
|
||||||
|
<div class="value">${(analysis.memory_usage / (1024*1024*1024)).toFixed(2)} GiB</div>
|
||||||
|
</div>
|
||||||
|
<div class="historical-stat">
|
||||||
|
<h4>CPU Utilization</h4>
|
||||||
|
<div class="value" style="color: ${analysis.cpu_utilization > 80 ? '#dc3545' : analysis.cpu_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.cpu_utilization.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="historical-stat">
|
||||||
|
<h4>Memory Utilization</h4>
|
||||||
|
<div class="value" style="color: ${analysis.memory_utilization > 80 ? '#dc3545' : analysis.memory_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.memory_utilization.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (recommendations.length > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="historical-summary">
|
||||||
|
<h3>Recommendations</h3>
|
||||||
|
`;
|
||||||
|
|
||||||
|
recommendations.forEach(rec => {
|
||||||
|
const severityClass = rec.severity;
|
||||||
|
html += `
|
||||||
|
<div class="historical-validation ${severityClass}">
|
||||||
|
<div class="validation-header">
|
||||||
|
<span class="severity-badge severity-${severityClass}">${severityClass}</span>
|
||||||
|
${rec.message}
|
||||||
|
</div>
|
||||||
|
<div class="validation-recommendation">
|
||||||
|
<strong>Recommendation:</strong> ${rec.recommendation}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
} else {
|
||||||
|
html += `
|
||||||
|
<div class="historical-summary">
|
||||||
|
<h3>Recommendations</h3>
|
||||||
|
<p>No specific recommendations at this time. Resource utilization appears to be within normal ranges.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHistoricalModal(namespace) {
|
||||||
|
document.getElementById('historicalModalTitle').textContent = `Historical Analysis - ${namespace}`;
|
||||||
|
document.getElementById('historicalModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeHistoricalModal() {
|
||||||
|
document.getElementById('historicalModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
document.getElementById('historicalModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeHistoricalModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user