Adds a /metrics page with Chart.js charts, which requires an annoying CDN fetch but until I figure out a good method of fetching things during build it's our best bet. I've pinned the thing so it's probably good. The page displays build counts, duration percentiles and system distribution. Time range and project filters are included, and the metrics page is linked from navigation. Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I99059594c29a9b35d2fd4d140628d6f46a6a6964
407 lines
9.7 KiB
HTML
407 lines
9.7 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Metrics - FC CI{% endblock %}
|
|
|
|
{% block auth %}
|
|
{% if !auth_name.is_empty() %}
|
|
<span class="auth-user">{{ auth_name }}</span>
|
|
<form method="POST" action="/logout"><button type="submit">Logout</button></form>
|
|
{% else %}
|
|
<a href="/login">Login</a>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1>Build Metrics Dashboard</h1>
|
|
|
|
{% if is_admin %}
|
|
<p class="admin-notice">Showing system-wide metrics. Filter by project below to view specific metrics.</p>
|
|
{% endif %}
|
|
|
|
<div class="metrics-controls">
|
|
<label for="time-range">Time Range:</label>
|
|
<select id="time-range">
|
|
<option value="24">Last 24 hours</option>
|
|
<option value="48">Last 48 hours</option>
|
|
<option value="168">Last 7 days</option>
|
|
</select>
|
|
|
|
<label for="project-filter">Project:</label>
|
|
<select id="project-filter">
|
|
<option value="">All Projects</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="metrics-grid">
|
|
<div class="metric-card">
|
|
<h3>Build Counts Over Time</h3>
|
|
<canvas id="builds-chart"></canvas>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<h3>Build Duration Percentiles</h3>
|
|
<canvas id="duration-chart"></canvas>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<h3>Builds by System</h3>
|
|
<canvas id="systems-chart"></canvas>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<h3>Success Rate Trend</h3>
|
|
<canvas id="success-rate-chart"></canvas>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script
|
|
src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"
|
|
integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ"
|
|
crossorigin="anonymous">
|
|
</script>
|
|
<script>
|
|
// Configuration
|
|
const API_BASE = '/api/v1/metrics';
|
|
let currentHours = 24;
|
|
let currentProjectId = null;
|
|
|
|
// Chart instances
|
|
let buildsChart, durationChart, systemsChart, successRateChart;
|
|
|
|
// Initialize charts on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initCharts();
|
|
loadData();
|
|
setupEventListeners();
|
|
loadProjects();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('time-range').addEventListener('change', function(e) {
|
|
currentHours = parseInt(e.target.value);
|
|
loadData();
|
|
});
|
|
|
|
document.getElementById('project-filter').addEventListener('change', function(e) {
|
|
currentProjectId = e.target.value || null;
|
|
loadData();
|
|
});
|
|
}
|
|
|
|
async function loadProjects() {
|
|
try {
|
|
const response = await fetch('/api/v1/projects');
|
|
const data = await response.json();
|
|
const select = document.getElementById('project-filter');
|
|
|
|
data.data.forEach(project => {
|
|
const option = document.createElement('option');
|
|
option.value = project.id;
|
|
option.textContent = project.name;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load projects:', error);
|
|
}
|
|
}
|
|
|
|
async function loadData() {
|
|
const params = new URLSearchParams();
|
|
params.set('hours', currentHours);
|
|
params.set('bucket', 60);
|
|
if (currentProjectId) {
|
|
params.set('project_id', currentProjectId);
|
|
}
|
|
|
|
try {
|
|
// Load build stats
|
|
const buildsResponse = await fetch(`${API_BASE}/timeseries/builds?${params}`);
|
|
const buildsData = await buildsResponse.json();
|
|
updateBuildsChart(buildsData);
|
|
updateSuccessRateChart(buildsData);
|
|
|
|
// Load duration percentiles
|
|
const durationResponse = await fetch(`${API_BASE}/timeseries/duration?${params}`);
|
|
const durationData = await durationResponse.json();
|
|
updateDurationChart(durationData);
|
|
|
|
// Load system distribution
|
|
const systemsResponse = await fetch(`${API_BASE}/systems?${params}`);
|
|
const systemsData = await systemsResponse.json();
|
|
updateSystemsChart(systemsData);
|
|
} catch (error) {
|
|
console.error('Failed to load metrics data:', error);
|
|
showError(document.querySelector('.metrics-grid'), 'Failed to load metrics data. Please try again later.');
|
|
}
|
|
}
|
|
|
|
function initCharts() {
|
|
Chart.defaults.font.family = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
Chart.defaults.color = '#666';
|
|
|
|
// Build counts chart
|
|
const buildsCtx = document.getElementById('builds-chart').getContext('2d');
|
|
buildsChart = new Chart(buildsCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'Successful',
|
|
data: [],
|
|
backgroundColor: '#28a745',
|
|
borderRadius: 4
|
|
},
|
|
{
|
|
label: 'Failed',
|
|
data: [],
|
|
backgroundColor: '#dc3545',
|
|
borderRadius: 4
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
stacked: true,
|
|
grid: { display: false }
|
|
},
|
|
y: {
|
|
stacked: true,
|
|
beginAtZero: true,
|
|
ticks: { stepSize: 1 }
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { position: 'top' }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Duration percentiles chart
|
|
const durationCtx = document.getElementById('duration-chart').getContext('2d');
|
|
durationChart = new Chart(durationCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'P50 (Median)',
|
|
data: [],
|
|
borderColor: '#007bff',
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
fill: true,
|
|
tension: 0.3
|
|
},
|
|
{
|
|
label: 'P95',
|
|
data: [],
|
|
borderColor: '#ffc107',
|
|
backgroundColor: 'rgba(255, 193, 7, 0.1)',
|
|
fill: true,
|
|
tension: 0.3
|
|
},
|
|
{
|
|
label: 'P99',
|
|
data: [],
|
|
borderColor: '#dc3545',
|
|
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
|
fill: true,
|
|
tension: 0.3
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Duration (seconds)'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { position: 'top' }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Systems chart
|
|
const systemsCtx = document.getElementById('systems-chart').getContext('2d');
|
|
systemsChart = new Chart(systemsCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: [
|
|
'#007bff',
|
|
'#28a745',
|
|
'#ffc107',
|
|
'#dc3545',
|
|
'#6f42c1',
|
|
'#20c997',
|
|
'#fd7e14',
|
|
'#e83e8c'
|
|
]
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'right' }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Success rate chart
|
|
const successCtx = document.getElementById('success-rate-chart').getContext('2d');
|
|
successRateChart = new Chart(successCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'Success Rate (%)',
|
|
data: [],
|
|
borderColor: '#28a745',
|
|
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
fill: true,
|
|
tension: 0.3
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value + '%';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { position: 'top' }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateBuildsChart(data) {
|
|
const labels = data.timestamps.map(t => {
|
|
const date = new Date(t);
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
});
|
|
|
|
const successful = data.total.map((total, i) => total - data.failed[i]);
|
|
|
|
buildsChart.data.labels = labels;
|
|
buildsChart.data.datasets[0].data = successful;
|
|
buildsChart.data.datasets[1].data = data.failed;
|
|
buildsChart.update();
|
|
}
|
|
|
|
function updateDurationChart(data) {
|
|
const labels = data.timestamps.map(t => {
|
|
const date = new Date(t);
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
});
|
|
|
|
durationChart.data.labels = labels;
|
|
durationChart.data.datasets[0].data = data.p50;
|
|
durationChart.data.datasets[1].data = data.p95;
|
|
durationChart.data.datasets[2].data = data.p99;
|
|
durationChart.update();
|
|
}
|
|
|
|
function updateSystemsChart(data) {
|
|
systemsChart.data.labels = data.systems;
|
|
systemsChart.data.datasets[0].data = data.counts;
|
|
systemsChart.update();
|
|
}
|
|
|
|
function updateSuccessRateChart(data) {
|
|
const labels = data.timestamps.map(t => {
|
|
const date = new Date(t);
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
});
|
|
|
|
const successRates = data.total.map((total, i) => {
|
|
if (total === 0) return 0;
|
|
return ((total - data.failed[i]) / total * 100).toFixed(1);
|
|
});
|
|
|
|
successRateChart.data.labels = labels;
|
|
successRateChart.data.datasets[0].data = successRates;
|
|
successRateChart.update();
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.metrics-controls {
|
|
background: #f8f9fa;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 1.5rem;
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.metrics-controls label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.metrics-controls select {
|
|
padding: 0.375rem 0.75rem;
|
|
border: 1px solid #ced4da;
|
|
border-radius: 4px;
|
|
background-color: #fff;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.metric-card {
|
|
background: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.metric-card h3 {
|
|
margin-top: 0;
|
|
margin-bottom: 1rem;
|
|
font-size: 1.1rem;
|
|
color: #212529;
|
|
}
|
|
|
|
.metric-card canvas {
|
|
max-height: 300px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.metrics-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|