Files
Archive/echo/internal/web/js/metrics.js
2024-09-02 20:34:11 +02:00

394 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const MetricsModule = (function () {
// Constants
const API_BASE_URL = '/api/v1';
const NODE_METRICS_PATH = '/node_metrics/';
const BYTE_TO_MB = 1024 * 1024;
const handleError = (error) => {
console.error('Error:', error);
};
// API functions
const fetchData = async (path, params = {}) => {
const url = new URL(API_BASE_URL + path, window.location.origin);
Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value));
try {
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
handleError(error);
return null;
}
};
const fetchLatestMetric = () => fetchData(NODE_METRICS_PATH, { latest: true }).then((data) => data?.data[0]);
const fetchMetrics = (startTs, endTs) => fetchData(NODE_METRICS_PATH, { start_ts: startTs, end_ts: endTs }).then((data) => data?.data);
// Chart functions
const initChart = (canvasId, type, datasets, legendPosition = '', yDisplayText = '', title = '', unit = '') => {
const ctx = $(`#${canvasId}`)[0].getContext('2d');
const colors = {
cpu: 'rgba(255, 99, 132, 1)',
memory: 'rgba(54, 162, 235, 1)',
disk: 'rgba(255, 206, 86, 1)',
receive: 'rgba(0, 150, 255, 1)',
transmit: 'rgba(255, 140, 0, 1)',
};
const getDatasetConfig = (label) => {
const color = colors[label.toLowerCase()] || 'rgba(0, 0, 0, 1)';
return {
label,
borderColor: color,
backgroundColor: color.replace('1)', '0.2)'),
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 2,
fill: true,
data: [],
};
};
const data = {
labels: [],
datasets: $.isArray(datasets) ? datasets.map((dataset) => getDatasetConfig(dataset.label)) : [getDatasetConfig(datasets.label)],
};
return new Chart(ctx, {
type,
data,
options: {
line: {
spanGaps: false, // 设置为 false不连接空值
},
responsive: true,
plugins: {
legend: { position: legendPosition },
title: {
display: !!title,
text: title,
position: 'bottom',
font: { size: 14, weight: 'bold' },
},
tooltip: {
callbacks: {
title: function (tooltipItems) {
return new Date(tooltipItems[0].label).toLocaleString();
},
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(2) + ' ' + unit;
}
return label;
},
},
},
},
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
displayFormats: {
minute: 'HH:mm',
},
},
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 10,
},
adapters: {
date: {
locale: 'en',
},
},
},
y: {
beginAtZero: true,
title: { display: true, text: yDisplayText, font: { weight: 'bold' } },
},
},
elements: { line: { tension: 0.4 } },
downsample: {
enabled: true,
threshold: 200,
},
},
});
};
const updateChart = (chart, newData, labels) => {
if (!newData || !labels) {
console.error('Invalid data or labels provided');
return;
}
if ($.isArray(newData) && $.isArray(newData[0])) {
$.each(chart.data.datasets, (index, dataset) => {
if (newData[index]) {
dataset.data = newData[index].map((value, i) => ({ x: moment(labels[i]), y: value }));
}
});
} else {
chart.data.datasets[0].data = newData.map((value, i) => ({ x: moment(labels[i]), y: value }));
}
chart.options.scales.x.min = moment(labels[0]);
chart.options.scales.x.max = moment(labels[labels.length - 1]);
chart.update();
};
const updateCharts = (charts, metrics, startTs, endTs) => {
console.log('Raw metrics data:', metrics);
const generateTimestamps = (start, end) => {
const timestamps = [];
let current = moment.unix(start);
const endMoment = moment.unix(end);
while (current.isSameOrBefore(endMoment)) {
timestamps.push(current.toISOString());
current.add(1, 'minute');
}
return timestamps;
};
const timestamps = generateTimestamps(startTs, endTs);
const processData = (dataKey) => {
const data = new Array(timestamps.length).fill(null);
metrics.forEach((metric) => {
const index = Math.floor((metric.timestamp - startTs) / 60);
if (index >= 0 && index < data.length) {
data[index] = metric[dataKey];
}
});
return data;
};
updateChart(charts.cpu, processData('cpu_usage'), timestamps);
updateChart(charts.memory, processData('memory_usage'), timestamps);
updateChart(charts.disk, processData('disk_usage'), timestamps);
updateChart(
charts.network,
[
processData('network_in').map((v) => (v === null ? null : v / BYTE_TO_MB)),
processData('network_out').map((v) => (v === null ? null : v / BYTE_TO_MB)),
],
timestamps
);
};
const addLatestDataToCharts = (charts, latestMetric) => {
console.log('Raw latestMetric data:', latestMetric);
const timestamp = moment.unix(latestMetric.timestamp);
$.each(charts, (key, chart) => {
// 检查是否已经有这个时间戳的数据
const existingDataIndex = chart.data.labels.findIndex((label) => label.isSame(timestamp));
if (existingDataIndex === -1) {
// 如果是新数据,添加到末尾
chart.data.labels.push(timestamp);
if (key === 'network') {
chart.data.datasets[0].data.push({ x: timestamp, y: latestMetric.network_in / BYTE_TO_MB });
chart.data.datasets[1].data.push({ x: timestamp, y: latestMetric.network_out / BYTE_TO_MB });
} else {
chart.data.datasets[0].data.push({ x: timestamp, y: latestMetric[`${key}_usage`] });
}
// 更新x轴范围但保持一定的时间窗口
const timeWindow = moment.duration(30, 'minutes'); // 设置显示的时间窗口例如30分钟
const oldestAllowedTime = moment(timestamp).subtract(timeWindow);
chart.options.scales.x.min = oldestAllowedTime;
chart.options.scales.x.max = timestamp;
// 开启图表的平移和缩放功能
chart.options.plugins.zoom = {
pan: {
enabled: true,
mode: 'x',
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true,
},
mode: 'x',
},
};
chart.update();
}
// 如果数据已存在,我们不做任何操作,保持现有数据
});
};
// Chart initialization
const initializeCharts = async () => {
const metric = await fetchLatestMetric();
if (!metric) return null;
return {
cpu: initChart('cpuChart', 'line', { label: 'CPU' }, 'top', 'Usage (%)', `CPU`, '%'),
memory: initChart('memoryChart', 'line', { label: 'Memory' }, 'top', 'Usage (%)', `Memory`, '%'),
disk: initChart('diskChart', 'line', { label: 'Disk' }, 'top', 'Usage (%)', `Disk`, '%'),
network: initChart(
'networkChart',
'line',
[{ label: 'Receive' }, { label: 'Transmit' }],
'top',
'Rate (MB/s)',
'Network Rate',
'MB/s'
),
};
};
// Date range functions
const setupDateRangeDropdown = (charts) => {
const $dateRangeDropdown = $('#dateRangeDropdown');
const $dateRangeButton = $('#dateRangeButton');
const $dateRangeText = $('#dateRangeText');
const $dateRangeInput = $('#dateRangeInput');
$dateRangeDropdown.find('.dropdown-item[data-range]').on('click', function (e) {
e.preventDefault();
const range = $(this).data('range');
const now = new Date();
let start, end;
switch (range) {
case '30m':
start = new Date(now - 30 * 60 * 1000);
break;
case '1h':
start = new Date(now - 60 * 60 * 1000);
break;
case '3h':
start = new Date(now - 3 * 60 * 60 * 1000);
break;
case '6h':
start = new Date(now - 6 * 60 * 60 * 1000);
break;
case '12h':
start = new Date(now - 12 * 60 * 60 * 1000);
break;
case '24h':
start = new Date(now - 24 * 60 * 60 * 1000);
break;
case '7d':
start = new Date(now - 7 * 24 * 60 * 60 * 1000);
break;
}
end = now;
const startTs = Math.floor(start.getTime() / 1000);
const endTs = Math.floor(end.getTime() / 1000);
fetchDataForRange(charts, startTs, endTs);
$dateRangeText.text($(this).text());
$dateRangeDropdown.removeClass('is-active');
});
$dateRangeButton.on('click', (event) => {
event.stopPropagation();
$dateRangeDropdown.toggleClass('is-active');
});
$(document).on('click', (event) => {
if (!$dateRangeDropdown.has(event.target).length) {
$dateRangeDropdown.removeClass('is-active');
}
});
const picker = flatpickr($dateRangeInput[0], {
mode: 'range',
enableTime: true,
dateFormat: 'Y-m-d H:i',
onChange: function (selectedDates) {
if (selectedDates.length === 2) {
const startTs = Math.floor(selectedDates[0].getTime() / 1000);
const endTs = Math.floor(selectedDates[1].getTime() / 1000);
fetchDataForRange(charts, startTs, endTs);
const formattedStart = selectedDates[0].toLocaleString();
const formattedEnd = selectedDates[1].toLocaleString();
$dateRangeText.text(`${formattedStart} - ${formattedEnd}`);
// 关闭下拉菜单
$dateRangeDropdown.removeClass('is-active');
}
},
onClose: function () {
// 确保在日期选择器关闭时也关闭下拉菜单
$dateRangeDropdown.removeClass('is-active');
},
});
// 防止点击日期选择器时关闭下拉菜单
$dateRangeInput.on('click', (event) => {
event.stopPropagation();
});
};
const fetchDataForRange = async (charts, startTs, endTs) => {
const metrics = await fetchMetrics(startTs, endTs);
if (metrics) {
console.log('Raw metrics data:', metrics);
updateCharts(charts, metrics, startTs, endTs);
}
};
// Auto refresh functions
const setupAutoRefresh = (charts) => {
let autoRefreshInterval;
let isAutoRefreshing = false;
$('#refreshButton').click(function () {
if (isAutoRefreshing) {
clearInterval(autoRefreshInterval);
$(this).removeClass('is-info');
$(this).find('span:last').text('Auto Refresh');
isAutoRefreshing = false;
} else {
$(this).addClass('is-info');
$(this).find('span:last').text('Stop Refresh');
isAutoRefreshing = true;
refreshData(charts);
autoRefreshInterval = setInterval(() => refreshData(charts), 5000);
}
});
};
const refreshData = async (charts) => {
const latestMetric = await fetchLatestMetric();
if (latestMetric) {
addLatestDataToCharts(charts, latestMetric);
}
};
// Main initialization function
const init = async () => {
const charts = await initializeCharts();
if (charts) {
setupDateRangeDropdown(charts);
setupAutoRefresh(charts);
}
};
// Public API
return {
init: init,
};
})();
// Initialize when the DOM is ready
document.addEventListener('DOMContentLoaded', MetricsModule.init);