mirror of
https://github.com/bolucat/Archive.git
synced 2025-10-05 16:18:04 +08:00
394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
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);
|