Files
FendxPHP/public/monitor/dashboard.js

650 lines
24 KiB
JavaScript
Raw Normal View History

// 全局变量
let currentSection = 'dashboard';
let refreshInterval = null;
let charts = {};
// API基础URL
const API_BASE = '/monitor';
// 初始化
document.addEventListener('DOMContentLoaded', function() {
initializeDashboard();
startAutoRefresh();
});
// 初始化仪表盘
function initializeDashboard() {
showSection('dashboard');
loadDashboardData();
}
// 显示指定区域
function showSection(section) {
// 隐藏所有区域
document.querySelectorAll('.section').forEach(el => {
el.classList.add('hidden');
});
// 显示指定区域
document.getElementById(section + '-section').classList.remove('hidden');
// 更新侧边栏状态
document.querySelectorAll('.sidebar-item').forEach(el => {
el.classList.remove('active');
});
event.currentTarget.classList.add('active');
// 更新标题
const titles = {
'dashboard': '仪表盘',
'health': '健康检查',
'metrics': '性能指标',
'errors': '错误追踪',
'logs': '日志分析',
'alerts': '告警管理',
'settings': '系统设置'
};
document.getElementById('section-title').textContent = titles[section];
currentSection = section;
// 加载对应数据
loadSectionData(section);
}
// 加载区域数据
function loadSectionData(section) {
switch(section) {
case 'dashboard':
loadDashboardData();
break;
case 'health':
loadHealthData();
break;
case 'metrics':
loadMetricsData();
break;
case 'errors':
loadErrorsData();
break;
case 'logs':
// 日志数据需要用户主动搜索
break;
case 'alerts':
loadAlertsData();
break;
case 'settings':
loadSettingsData();
break;
}
}
// 加载仪表盘数据
async function loadDashboardData() {
try {
// 加载健康状态
const healthResponse = await fetch(`${API_BASE}/health`);
const health = await healthResponse.json();
updateDashboardHealth(health.data);
// 加载指标数据
const metricsResponse = await fetch(`${API_BASE}/metrics`);
const metrics = await metricsResponse.json();
updateDashboardMetrics(metrics.data);
// 加载告警数据
const alertsResponse = await fetch(`${API_BASE}/alerts/active`);
const alerts = await alertsResponse.json();
updateDashboardAlerts(alerts.data);
// 加载图表数据
loadDashboardCharts();
// 更新时间
updateLastUpdateTime();
} catch (error) {
console.error('加载仪表盘数据失败:', error);
showError('加载仪表盘数据失败');
}
}
// 更新仪表盘健康状态
function updateDashboardHealth(health) {
const statusEl = document.getElementById('system-status');
const statusClass = health.status === 'healthy' ? 'status-healthy' :
health.status === 'warning' ? 'status-warning' : 'status-critical';
statusEl.textContent = health.status === 'healthy' ? '健康' :
health.status === 'warning' ? '警告' : '严重';
statusEl.className = `text-2xl font-bold ${statusClass}`;
}
// 更新仪表盘指标
function updateDashboardMetrics(metrics) {
if (metrics.system) {
const memoryUsage = metrics.system.memory_usage;
const memoryLimit = metrics.system.memory_limit;
const memoryPercent = memoryLimit > 0 ? (memoryUsage / memoryLimit * 100).toFixed(1) : 0;
document.getElementById('memory-usage').textContent = memoryPercent + '%';
}
// 计算平均响应时间
if (metrics.histograms && metrics.histograms.http_request_duration) {
const avgTime = metrics.histograms.http_request_duration.mean || 0;
document.getElementById('response-time').textContent = avgTime.toFixed(2) + 'ms';
}
}
// 更新仪表盘告警
function updateDashboardAlerts(alerts) {
document.getElementById('active-alerts').textContent = alerts.length;
const recentAlertsEl = document.getElementById('recent-alerts');
if (alerts.length === 0) {
recentAlertsEl.innerHTML = '<p class="text-gray-500 text-center py-4">暂无告警</p>';
} else {
recentAlertsEl.innerHTML = alerts.slice(0, 5).map(alert => `
<div class="alert-item flex items-center justify-between p-3 bg-${alert.severity === 'critical' ? 'red' : 'yellow'}-50 border border-${alert.severity === 'critical' ? 'red' : 'yellow'}-200 rounded-lg">
<div class="flex items-center">
<div class="w-2 h-2 bg-${alert.severity === 'critical' ? 'red' : 'yellow'}-500 rounded-full mr-3"></div>
<div>
<p class="font-medium text-gray-800">${alert.type}</p>
<p class="text-sm text-gray-600">${alert.message}</p>
</div>
</div>
<div class="text-right">
<p class="text-xs text-gray-500">${formatTime(alert.timestamp)}</p>
<button onclick="acknowledgeAlert('${alert.id}')" class="text-xs text-blue-600 hover:text-blue-800">确认</button>
</div>
</div>
`).join('');
}
}
// 加载仪表盘图表
async function loadDashboardCharts() {
try {
// 加载请求数据
const statsResponse = await fetch(`${API_BASE}/stats`);
const stats = await statsResponse.json();
// 请求趋势图
if (charts.requests) {
charts.requests.destroy();
}
const requestsChart = {
series: [{
name: '请求数',
data: generateTimeSeriesData()
}],
chart: {
type: 'area',
height: 300,
toolbar: { show: false }
},
dataLabels: { enabled: false },
stroke: { curve: 'smooth', width: 2 },
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.3
}
},
xaxis: {
categories: generateTimeLabels()
},
yaxis: { title: { text: '请求数' } },
colors: ['#3b82f6']
};
charts.requests = new ApexCharts(document.getElementById('requests-chart'), requestsChart);
charts.requests.render();
// 错误分布图
if (charts.errors) {
charts.errors.destroy();
}
const errorsData = stats.data?.system?.error_count || 0;
const errorsChart = {
series: [errorsData, Math.max(0, 100 - errorsData)],
chart: {
type: 'donut',
height: 300
},
labels: ['错误', '正常'],
colors: ['#ef4444', '#10b981'],
legend: { position: 'bottom' },
dataLabels: { enabled: true }
};
charts.errors = new ApexCharts(document.getElementById('errors-chart'), errorsChart);
charts.errors.render();
} catch (error) {
console.error('加载图表失败:', error);
}
}
// 加载健康检查数据
async function loadHealthData() {
try {
const response = await fetch(`${API_BASE}/health`);
const result = await response.json();
const healthChecksEl = document.getElementById('health-checks');
const checks = result.data.checks;
healthChecksEl.innerHTML = Object.entries(checks).map(([name, check]) => `
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="flex items-center">
<div class="w-3 h-3 bg-${check.status === 'healthy' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}-500 rounded-full mr-3"></div>
<div>
<p class="font-medium text-gray-800">${getComponentName(name)}</p>
<p class="text-sm text-gray-600">${check.message || '运行正常'}</p>
</div>
</div>
<div class="text-right">
<span class="px-2 py-1 text-xs font-medium rounded-full bg-${check.status === 'healthy' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}-100 text-${check.status === 'healthy' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}-800">
${check.status === 'healthy' ? '健康' : check.status === 'warning' ? '警告' : '严重'}
</span>
<p class="text-xs text-gray-500 mt-1">${check.duration ? check.duration.toFixed(3) + 's' : ''}</p>
</div>
</div>
`).join('');
} catch (error) {
console.error('加载健康检查数据失败:', error);
showError('加载健康检查数据失败');
}
}
// 加载指标数据
async function loadMetricsData() {
try {
const response = await fetch(`${API_BASE}/metrics`);
const result = await response.json();
const metrics = result.data;
// 系统指标
const systemMetricsEl = document.getElementById('system-metrics');
if (metrics.system) {
systemMetricsEl.innerHTML = `
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600">内存使用</span>
<span class="font-medium">${formatBytes(metrics.system.memory_usage)} / ${formatBytes(metrics.system.memory_limit)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">CPU使用率</span>
<span class="font-medium">${(metrics.system.cpu_usage * 100).toFixed(1)}%</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">运行时间</span>
<span class="font-medium">${formatUptime(metrics.system.uptime)}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">负载均衡</span>
<span class="font-medium">${metrics.system.load_avg ? metrics.system.load_avg[0].toFixed(2) : 'N/A'}</span>
</div>
</div>
`;
}
// 应用指标
const appMetricsEl = document.getElementById('application-metrics');
const totalRequests = Object.values(metrics.counters || {}).reduce((sum, count) => sum + count, 0);
const totalErrors = Object.values(metrics.counters || {}).filter(key => key.includes('error')).reduce((sum, count) => sum + count, 0);
appMetricsEl.innerHTML = `
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600">总请求数</span>
<span class="font-medium">${totalRequests}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总错误数</span>
<span class="font-medium">${totalErrors}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">成功率</span>
<span class="font-medium">${totalRequests > 0 ? ((totalRequests - totalErrors) / totalRequests * 100).toFixed(1) : 100}%</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">活跃连接</span>
<span class="font-medium">${metrics.gauges?.requests_in_progress || 0}</span>
</div>
</div>
`;
} catch (error) {
console.error('加载指标数据失败:', error);
showError('加载指标数据失败');
}
}
// 加载错误数据
async function loadErrorsData() {
try {
const filter = document.getElementById('error-filter').value;
const response = await fetch(`${API_BASE}/errors${filter ? '?severity=' + filter : ''}`);
const result = await response.json();
const errorsListEl = document.getElementById('errors-list');
if (result.data.length === 0) {
errorsListEl.innerHTML = '<p class="text-gray-500 text-center py-4">暂无错误记录</p>';
} else {
errorsListEl.innerHTML = result.data.map(error => `
<div class="border rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center mb-2">
<span class="px-2 py-1 text-xs font-medium rounded-full bg-${error.severity === 'critical' ? 'red' : error.severity === 'warning' ? 'yellow' : 'blue'}-100 text-${error.severity === 'critical' ? 'red' : error.severity === 'warning' ? 'yellow' : 'blue'}-800">
${error.severity.toUpperCase()}
</span>
<span class="ml-2 text-sm text-gray-500">${error.type}</span>
</div>
<p class="text-gray-800 mb-2">${error.message}</p>
<div class="text-sm text-gray-600">
<p>文件: ${error.file || 'N/A'}:${error.line || 'N/A'}</p>
<p>时间: ${formatTime(error.timestamp)}</p>
<p>Trace ID: ${error.trace_id || 'N/A'}</p>
</div>
</div>
</div>
</div>
`).join('');
}
} catch (error) {
console.error('加载错误数据失败:', error);
showError('加载错误数据失败');
}
}
// 搜索日志
async function searchLogs() {
const level = document.getElementById('log-level').value;
const search = document.getElementById('log-search').value;
const params = new URLSearchParams();
if (level) params.append('level', level);
if (search) params.append('message', search);
params.append('limit', '50');
try {
const response = await fetch(`${API_BASE}/logs/search?${params}`);
const result = await response.json();
const logsContainerEl = document.getElementById('logs-container');
if (result.data.logs.length === 0) {
logsContainerEl.innerHTML = '<p class="text-gray-500 text-center py-4">未找到匹配的日志</p>';
} else {
logsContainerEl.innerHTML = result.data.logs.map(log => `
<div class="border-l-4 border-${getLogLevelColor(log.level)} pl-4 py-2">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="px-2 py-1 text-xs font-medium rounded bg-${getLogLevelColor(log.level)}-100 text-${getLogLevelColor(log.level)}-800">
${log.level}
</span>
<span class="ml-2 text-sm text-gray-500">${log.datetime}</span>
</div>
<span class="text-xs text-gray-500">${log.trace_id || ''}</span>
</div>
<p class="text-gray-800 mt-1">${log.message}</p>
</div>
`).join('');
}
} catch (error) {
console.error('搜索日志失败:', error);
showError('搜索日志失败');
}
}
// 加载告警数据
async function loadAlertsData() {
try {
const status = document.getElementById('alert-status').value;
const response = await fetch(`${API_BASE}/alerts${status ? '?status=' + status : ''}`);
const result = await response.json();
const alertsListEl = document.getElementById('alerts-list');
if (result.data.length === 0) {
alertsListEl.innerHTML = '<p class="text-gray-500 text-center py-4">暂无告警</p>';
} else {
alertsListEl.innerHTML = result.data.map(alert => `
<div class="border rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center mb-2">
<div class="w-3 h-3 bg-${alert.severity === 'critical' ? 'red' : 'yellow'}-500 rounded-full mr-2"></div>
<span class="font-medium text-gray-800">${alert.type}</span>
<span class="ml-2 px-2 py-1 text-xs font-medium rounded-full bg-${alert.status === 'active' ? 'red' : alert.status === 'acknowledged' ? 'yellow' : 'green'}-100 text-${alert.status === 'active' ? 'red' : alert.status === 'acknowledged' ? 'yellow' : 'green'}-800">
${alert.status === 'active' ? '活跃' : alert.status === 'acknowledged' ? '已确认' : '已解决'}
</span>
</div>
<p class="text-gray-800 mb-2">${alert.message}</p>
<p class="text-sm text-gray-500">时间: ${formatTime(alert.timestamp)}</p>
</div>
<div class="flex space-x-2">
${alert.status === 'active' ? `
<button onclick="acknowledgeAlert('${alert.id}')" class="text-sm text-blue-600 hover:text-blue-800">确认</button>
<button onclick="resolveAlert('${alert.id}')" class="text-sm text-green-600 hover:text-green-800">解决</button>
` : ''}
</div>
</div>
</div>
`).join('');
}
} catch (error) {
console.error('加载告警数据失败:', error);
showError('加载告警数据失败');
}
}
// 加载设置数据
async function loadSettingsData() {
try {
// 这里可以加载实际的配置数据
// 暂时使用默认值
document.getElementById('sample-rate').value = '1.0';
document.getElementById('retention').value = '3600';
document.getElementById('error-threshold').value = '0.05';
document.getElementById('memory-threshold').value = '0.8';
} catch (error) {
console.error('加载设置数据失败:', error);
showError('加载设置数据失败');
}
}
// 确认告警
async function acknowledgeAlert(alertId) {
try {
const response = await fetch(`${API_BASE}/alerts/${alertId}/acknowledge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ acknowledged_by: 'admin' })
});
if (response.ok) {
showSuccess('告警已确认');
if (currentSection === 'dashboard') {
loadDashboardData();
} else if (currentSection === 'alerts') {
loadAlertsData();
}
} else {
showError('确认告警失败');
}
} catch (error) {
console.error('确认告警失败:', error);
showError('确认告警失败');
}
}
// 解决告警
async function resolveAlert(alertId) {
try {
const response = await fetch(`${API_BASE}/alerts/${alertId}/resolve`, {
method: 'POST'
});
if (response.ok) {
showSuccess('告警已解决');
if (currentSection === 'dashboard') {
loadDashboardData();
} else if (currentSection === 'alerts') {
loadAlertsData();
}
} else {
showError('解决告警失败');
}
} catch (error) {
console.error('解决告警失败:', error);
showError('解决告警失败');
}
}
// 保存设置
async function saveSettings() {
try {
const settings = {
sample_rate: parseFloat(document.getElementById('sample-rate').value),
retention: parseInt(document.getElementById('retention').value),
error_threshold: parseFloat(document.getElementById('error-threshold').value),
memory_threshold: parseFloat(document.getElementById('memory-threshold').value)
};
// 这里应该发送到后端保存
console.log('保存设置:', settings);
showSuccess('设置已保存');
} catch (error) {
console.error('保存设置失败:', error);
showError('保存设置失败');
}
}
// 刷新数据
function refreshData() {
loadSectionData(currentSection);
}
// 刷新错误数据
function refreshErrors() {
loadErrorsData();
}
// 刷新告警数据
function refreshAlerts() {
loadAlertsData();
}
// 开始自动刷新
function startAutoRefresh() {
refreshInterval = setInterval(() => {
if (currentSection !== 'settings') {
loadSectionData(currentSection);
}
}, 30000); // 30秒刷新一次
}
// 停止自动刷新
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// 更新最后更新时间
function updateLastUpdateTime() {
document.getElementById('last-update').textContent = '最后更新: ' + new Date().toLocaleTimeString();
}
// 工具函数
function formatTime(timestamp) {
return new Date(timestamp * 1000).toLocaleString();
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}${hours}小时`;
} else if (hours > 0) {
return `${hours}小时 ${minutes}分钟`;
} else {
return `${minutes}分钟`;
}
}
function getComponentName(name) {
const names = {
'database': '数据库',
'cache': '缓存',
'filesystem': '文件系统',
'memory': '内存',
'disk': '磁盘',
'external_services': '外部服务'
};
return names[name] || name;
}
function getLogLevelColor(level) {
const colors = {
'DEBUG': 'gray',
'INFO': 'blue',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red'
};
return colors[level] || 'gray';
}
function generateTimeSeriesData() {
// 生成模拟数据
return Array.from({length: 24}, () => Math.floor(Math.random() * 100) + 50);
}
function generateTimeLabels() {
const labels = [];
const now = new Date();
for (let i = 23; i >= 0; i--) {
const time = new Date(now.getTime() - i * 60 * 60 * 1000);
labels.push(time.getHours() + ':00');
}
return labels;
}
function showSuccess(message) {
// 显示成功消息
console.log('Success:', message);
}
function showError(message) {
// 显示错误消息
console.error('Error:', message);
}
// 页面卸载时清理
window.addEventListener('beforeunload', stopAutoRefresh);