Skip to content

Commit 5b6591e

Browse files
committed
templates: statistics page to monitor infrastructure error rate
This page will help to show approximate rate of infrastructure failures. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
1 parent 43ac06c commit 5b6591e

File tree

2 files changed

+361
-0
lines changed

2 files changed

+361
-0
lines changed

api/main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,23 @@ async def manage():
10131013
return PlainTextResponse(file.read(), headers=hdr)
10141014

10151015

1016+
@app.get('/stats')
1017+
async def stats_page():
1018+
"""Serve simple HTML page to view infrastructure statistics"""
1019+
metrics.add('http_requests_total', 1)
1020+
root_dir = os.path.dirname(os.path.abspath(__file__))
1021+
stats_path = os.path.join(root_dir, 'templates', 'stats.html')
1022+
with open(stats_path, 'r', encoding='utf-8') as file:
1023+
# set header to text/html and no-cache stuff
1024+
hdr = {
1025+
'Content-Type': 'text/html',
1026+
'Cache-Control': 'no-cache, no-store, must-revalidate',
1027+
'Pragma': 'no-cache',
1028+
'Expires': '0'
1029+
}
1030+
return PlainTextResponse(file.read(), headers=hdr)
1031+
1032+
10161033
@app.get('/icons/{icon_name}')
10171034
async def icons(icon_name: str):
10181035
"""Serve icons from /static/icons"""

api/templates/stats.html

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>KernelCI API Statistics</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<!-- Bootstrap CSS, TODO: check for latest version -->
8+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
9+
<!-- Bootstrap Icons, same here, check for latest version -->
10+
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
11+
<!-- Bootstrap JS -->
12+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
13+
<!-- jQuery -->
14+
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
15+
16+
<style>
17+
body {
18+
background-color: #f8f9fa;
19+
}
20+
.stats-container {
21+
max-width: 800px;
22+
margin: 0 auto;
23+
padding: 20px;
24+
}
25+
.stats-card {
26+
background: white;
27+
border-radius: 10px;
28+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
29+
padding: 30px;
30+
margin-bottom: 20px;
31+
}
32+
.form-section {
33+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
34+
color: white;
35+
border-radius: 10px;
36+
padding: 30px;
37+
margin-bottom: 30px;
38+
}
39+
.btn-generate {
40+
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
41+
border: none;
42+
padding: 12px 30px;
43+
font-weight: bold;
44+
transition: all 0.3s;
45+
}
46+
.btn-generate:hover {
47+
transform: translateY(-2px);
48+
box-shadow: 0 5px 15px rgba(238, 90, 36, 0.4);
49+
}
50+
.stat-item {
51+
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
52+
color: white;
53+
border-radius: 10px;
54+
padding: 20px;
55+
text-align: center;
56+
margin-bottom: 15px;
57+
transition: transform 0.3s;
58+
}
59+
.stat-item:hover {
60+
transform: translateY(-3px);
61+
}
62+
.stat-number {
63+
font-size: 2.5rem;
64+
font-weight: bold;
65+
margin-bottom: 5px;
66+
}
67+
.stat-label {
68+
font-size: 1.1rem;
69+
opacity: 0.9;
70+
}
71+
.result-breakdown {
72+
background: white;
73+
border-radius: 10px;
74+
padding: 20px;
75+
margin-top: 20px;
76+
}
77+
.result-item {
78+
display: flex;
79+
justify-content: space-between;
80+
align-items: center;
81+
padding: 10px 15px;
82+
margin: 5px 0;
83+
border-radius: 8px;
84+
font-weight: 500;
85+
}
86+
.result-pass {
87+
background-color: #d4edda;
88+
color: #155724;
89+
border-left: 4px solid #28a745;
90+
}
91+
.result-fail {
92+
background-color: #f8d7da;
93+
color: #721c24;
94+
border-left: 4px solid #dc3545;
95+
}
96+
.result-incomplete {
97+
background-color: #fff3cd;
98+
color: #856404;
99+
border-left: 4px solid #ffc107;
100+
}
101+
.result-null {
102+
background-color: #e2e3e5;
103+
color: #383d41;
104+
border-left: 4px solid #6c757d;
105+
}
106+
.loading-spinner {
107+
display: none;
108+
}
109+
.fade-in {
110+
animation: fadeIn 0.5s ease-in;
111+
}
112+
@keyframes fadeIn {
113+
from { opacity: 0; transform: translateY(20px); }
114+
to { opacity: 1; transform: translateY(0); }
115+
}
116+
</style>
117+
</head>
118+
<body>
119+
<div class="container-fluid">
120+
<div class="stats-container">
121+
<h1 class="text-center mb-4">
122+
<i class="bi bi-graph-up"></i> KernelCI Statistics
123+
</h1>
124+
125+
<div class="form-section">
126+
<h3 class="mb-3"><i class="bi bi-sliders"></i> Filter Options</h3>
127+
<div class="row">
128+
<div class="col-md-4 mb-3">
129+
<label for="duration" class="form-label">Duration</label>
130+
<select class="form-select" id="duration">
131+
<option value="24h">Last 24 hours</option>
132+
<option value="48h">Last 48 hours</option>
133+
<option value="7d">Last 7 days</option>
134+
</select>
135+
</div>
136+
<div class="col-md-4 mb-3">
137+
<label for="kind" class="form-label">Kind</label>
138+
<select class="form-select" id="kind">
139+
<option value="kbuild">Kernel Builds</option>
140+
<option value="job">Test Jobs</option>
141+
<option value="checkout">Checkouts</option>
142+
</select>
143+
</div>
144+
<div class="col-md-4 mb-3 d-flex align-items-end">
145+
<button type="button" class="btn btn-generate btn-primary w-100" id="generateBtn">
146+
<i class="bi bi-play-circle"></i> Generate Statistics
147+
</button>
148+
</div>
149+
</div>
150+
</div>
151+
152+
<div id="loadingSection" class="text-center loading-spinner">
153+
<div class="spinner-border text-primary" role="status">
154+
<span class="visually-hidden">Loading...</span>
155+
</div>
156+
<p class="mt-2">Fetching statistics...</p>
157+
</div>
158+
159+
<div id="statsResults" style="display: none;">
160+
<div class="stats-card fade-in">
161+
<h4 class="mb-4"><i class="bi bi-bar-chart"></i> Statistics Overview</h4>
162+
163+
<div class="row">
164+
<div class="col-md-12">
165+
<div class="stat-item">
166+
<div class="stat-number" id="totalNodes">-</div>
167+
<div class="stat-label">Total Nodes</div>
168+
</div>
169+
</div>
170+
</div>
171+
172+
<div class="result-breakdown">
173+
<h5 class="mb-3"><i class="bi bi-pie-chart"></i> Results Breakdown</h5>
174+
<div id="resultsContainer">
175+
<!-- Results will be populated here -->
176+
</div>
177+
</div>
178+
</div>
179+
</div>
180+
</div>
181+
</div>
182+
183+
<script>
184+
var apiurl;
185+
186+
function initializeAPI() {
187+
// Get API URL from current page URL, similar to viewer.html
188+
var pagebaseurl = window.location.href.split('?')[0];
189+
apiurl = pagebaseurl.replace('/stats', '');
190+
}
191+
192+
function getDurationFilter(duration) {
193+
var dateobj = new Date();
194+
195+
switch(duration) {
196+
case '24h':
197+
dateobj.setDate(dateobj.getDate() - 1);
198+
break;
199+
case '48h':
200+
dateobj.setDate(dateobj.getDate() - 2);
201+
break;
202+
case '7d':
203+
dateobj.setDate(dateobj.getDate() - 7);
204+
break;
205+
}
206+
207+
return dateobj.toISOString().split('.')[0];
208+
}
209+
210+
function showLoading() {
211+
$('#loadingSection').show();
212+
$('#statsResults').hide();
213+
}
214+
215+
function hideLoading() {
216+
$('#loadingSection').hide();
217+
}
218+
219+
function displayStatistics(data) {
220+
hideLoading();
221+
222+
// Calculate statistics
223+
var totalNodes = data.items.length;
224+
var resultStats = {};
225+
var listNodesResult = {};
226+
227+
// Count results
228+
data.items.forEach(function(node) {
229+
var result = node.result || 'null';
230+
resultStats[result] = (resultStats[result] || 0) + 1;
231+
// Store node in array, so we can generate a list later
232+
if (!listNodesResult[result]) {
233+
listNodesResult[result] = [];
234+
}
235+
listNodesResult[result].push(node);
236+
});
237+
238+
// Update total nodes
239+
$('#totalNodes').text(totalNodes);
240+
241+
// Update results breakdown
242+
var resultsHtml = '';
243+
var resultClasses = {
244+
'pass': 'result-pass',
245+
'fail': 'result-fail',
246+
'incomplete': 'result-incomplete',
247+
'null': 'result-null'
248+
};
249+
250+
var resultIcons = {
251+
'pass': 'bi-check-circle',
252+
'fail': 'bi-x-circle',
253+
'incomplete': 'bi-clock',
254+
'null': 'bi-question-circle'
255+
};
256+
257+
// Sort results for consistent display
258+
Object.keys(resultStats).sort().forEach(function(result) {
259+
var count = resultStats[result];
260+
var className = resultClasses[result] || 'result-null';
261+
var icon = resultIcons[result] || 'bi-question-circle';
262+
var percentage = totalNodes > 0 ? ((count / totalNodes) * 100).toFixed(1) : 0;
263+
264+
resultsHtml += `
265+
<div class="result-item ${className}" id="result-${result}">
266+
<span><i class="${icon}"></i> ${result}</span>
267+
<span><strong>${count}</strong> (${percentage}%)</span>
268+
</div>
269+
`;
270+
});
271+
272+
$('#resultsContainer').html(resultsHtml);
273+
// Add click handlers to result items, so we can show nodes for each result
274+
Object.keys(resultStats).forEach(function(result) {
275+
$('#result-' + result).click(function() {
276+
var nodes = listNodesResult[result] || [];
277+
if (nodes.length > 0) {
278+
var nodeListHtml = '<ul class="list-group">';
279+
nodes.forEach(function(node) {
280+
nodeListHtml += `<li class="list-group-item">
281+
<strong><a href="/viewer?node_id=${node.id}" target="_blank">${node.name}</a></strong> - ${node.created} - ${node.result || 'null'}
282+
</li>`;
283+
});
284+
nodeListHtml += '</ul>';
285+
$('#resultsContainer').append(`
286+
<div class="mt-3">
287+
<h6>Nodes with result "${result}":</h6>
288+
${nodeListHtml}
289+
</div>
290+
`);
291+
} else {
292+
$('#resultsContainer').append(`
293+
<div class="mt-3">
294+
<p>No nodes found with result "${result}".</p>
295+
</div>
296+
`);
297+
}
298+
});
299+
});
300+
$('#statsResults').show().addClass('fade-in');
301+
}
302+
303+
function generateStatistics() {
304+
var duration = $('#duration').val();
305+
var kind = $('#kind').val();
306+
307+
showLoading();
308+
309+
// Build API URL with filters
310+
var dateFilter = getDurationFilter(duration);
311+
var url = apiurl + '/latest/nodes?kind=' + encodeURIComponent(kind) +
312+
'&created__gt=' + encodeURIComponent(dateFilter) +
313+
'&limit=1000';
314+
315+
console.log('Fetching statistics from:', url);
316+
317+
$.ajax({
318+
url: url,
319+
method: 'GET',
320+
success: function(data) {
321+
displayStatistics(data);
322+
},
323+
error: function(xhr, status, error) {
324+
hideLoading();
325+
console.error('Error fetching statistics:', error);
326+
alert('Error fetching statistics: ' + error);
327+
}
328+
});
329+
}
330+
331+
// Initialize on page load
332+
$(document).ready(function() {
333+
initializeAPI();
334+
335+
$('#generateBtn').click(function() {
336+
generateStatistics();
337+
});
338+
339+
// Generate initial statistics
340+
generateStatistics();
341+
});
342+
</script>
343+
</body>
344+
</html>

0 commit comments

Comments
 (0)