diff --git a/reisa-admin/reisaAdminSpring/pom.xml b/reisa-admin/reisaAdminSpring/pom.xml index ce15028..fcf2cde 100644 --- a/reisa-admin/reisaAdminSpring/pom.xml +++ b/reisa-admin/reisaAdminSpring/pom.xml @@ -64,6 +64,23 @@ 0.1.55 + + + test + + true + + + test + + + + product + + product + + + diff --git a/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/api/ApiServerV1.java b/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/api/ApiServerV1.java index 15e1c09..ba4f9c4 100644 --- a/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/api/ApiServerV1.java +++ b/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/api/ApiServerV1.java @@ -20,6 +20,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Semaphore; import java.util.stream.Collectors; @RestController @@ -71,14 +72,6 @@ public class ApiServerV1 { } } } - @GetMapping("/status/history/{ip}") - public List getStatus(@PathVariable String ip, @RequestParam(defaultValue = "0", required = false) int limit) { - List statuses = statusDao.findByHostOrderByTimestampDesc(ip); - if (limit > 0) { - return statuses.stream().limit(limit).collect(Collectors.toList()); - } - return statuses; - } @GetMapping("/server") public List getAllServers() { @@ -138,4 +131,178 @@ public class ApiServerV1 { public void deleteServer(@RequestBody Server server) { serverDao.delete(server); } + + @GetMapping("/status/history/{ip}") + public List getStatus(@PathVariable String ip, + @RequestParam(defaultValue = "0000-00-00 00:00:00", required = false) String startDate, + @RequestParam(defaultValue = "9999-99-99 99:99:99", required = false) String endDate) { + System.out.println("getStatus: " + ip + "开始加载"); + List statuses = statusDao.findByHostOrderByTimestampDesc(ip); + System.out.println("getStatus: " + ip + "加载完成"); + // 时间范围过滤 + LocalDateTime start = parseDateTime(startDate); + LocalDateTime end = parseDateTime(endDate); + + List filteredStatuses = statuses.stream() + .filter(status -> status.getTimestamp() != null) + .filter(status -> !status.getTimestamp().isBefore(start)) + .filter(status -> !status.getTimestamp().isAfter(end)) + .collect(Collectors.toList()); + // 数据优化 - 移除变化较小的数据点 + List optimizedStatuses = optimizeStatusData(filteredStatuses); + System.out.println("getStatus: " + ip + "数据优化完成"); + System.out.print(optimizedStatuses.size()); + return optimizedStatuses; + } + @GetMapping("/status/history/all/{ips}") + public Map> getStatusIps(@PathVariable String ips, + @RequestParam(defaultValue = "0000-00-00 00:00:00", required = false) String startDate, + @RequestParam(defaultValue = "9999-99-99 99:99:99", required = false) String endDate) { + List ipList = Arrays.asList(ips.split(",")); + Map> optimizedStatuses = new ConcurrentHashMap<>(); + + // 限制并发数量,避免数据库连接耗尽 + Semaphore semaphore = new Semaphore(10); // 最多同时处理10个IP + + List> futures = ipList.stream() + .map(ip -> CompletableFuture.runAsync(() -> { + try { + semaphore.acquire(); // 获取许可 + List statuses = statusDao.findByHostOrderByTimestampDesc(ip); + + // 时间范围过滤 + LocalDateTime start = parseDateTime(startDate); + LocalDateTime end = parseDateTime(endDate); + + List filteredStatuses = statuses.stream() + .filter(status -> status.getTimestamp() != null) + .filter(status -> !status.getTimestamp().isBefore(start)) + .filter(status -> !status.getTimestamp().isAfter(end)) + .collect(Collectors.toList()); + + // 数据优化 - 移除变化较小的数据点 + List optimizedStatuse = optimizeStatusData(filteredStatuses); + optimizedStatuses.put(ip, optimizedStatuse); + } catch (Exception e) { + log.error("Error processing status for IP: {}", ip, e); + optimizedStatuses.put(ip, new ArrayList<>()); + } finally { + semaphore.release(); // 释放许可 + } + })) + .collect(Collectors.toList()); + + // 等待所有任务完成 + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + return optimizedStatuses; + } + + private LocalDateTime parseDateTime(String dateTimeStr) { + try { + // 处理默认值情况 + if ("0000-00-00 00:00:00".equals(dateTimeStr) || "9999-99-99 99:99:99".equals(dateTimeStr)) { + return "0000-00-00 00:00:00".equals(dateTimeStr) ? + LocalDateTime.MIN : LocalDateTime.MAX; + } + return LocalDateTime.parse(dateTimeStr.replace(" ", "T")); + } catch (Exception e) { + return LocalDateTime.now(); + } + } + + private List optimizeStatusData(List statuses) { + if (statuses.size() <= 2) { + return statuses; // 数据点太少无需优化 + } + + List result = new ArrayList<>(); + result.add(statuses.get(0)); // 始终保留第一个点 + + // 计算CPU和内存使用率变化的阈值(基于整体数据计算) + double cpuThreshold = calculateThreshold(statuses, Status::getCpuUsagePercent); + double memoryThreshold = calculateThreshold(statuses, Status::getMemoryUsagePercent); + + // 设置最小阈值,避免过度优化 + cpuThreshold = Math.max(cpuThreshold, 0.5); // 最小0.5% + memoryThreshold = Math.max(memoryThreshold, 0.5); // 最小0.5% + + Status previousStatus = statuses.get(0); + + for (int i = 1; i < statuses.size() - 1; i++) { + Status current = statuses.get(i); + + // 检查CPU或内存使用率是否有显著变化 + boolean significantChange = + hasSignificantChange(previousStatus, current, cpuThreshold, memoryThreshold); + + if (significantChange) { + result.add(current); + previousStatus = current; + } + } + + // 始终保留最后一个点 + if (!statuses.isEmpty() && !result.contains(statuses.get(statuses.size() - 1))) { + result.add(statuses.get(statuses.size() - 1)); + } + + return result; + } + + private boolean hasSignificantChange(Status prev, Status curr, + double cpuThreshold, double memoryThreshold) { + Double prevCpu = prev.getCpuUsagePercent(); + Double currCpu = curr.getCpuUsagePercent(); + Double prevMem = prev.getMemoryUsagePercent(); + Double currMem = curr.getMemoryUsagePercent(); + + // 如果任一值为空,则认为有变化 + if (prevCpu == null || currCpu == null || prevMem == null || currMem == null) { + return true; + } + + // 检查变化是否超过阈值 + return Math.abs(currCpu - prevCpu) >= cpuThreshold || + Math.abs(currMem - prevMem) >= memoryThreshold; + } + + // 在Status类中添加辅助方法获取使用率数值 + private static class StatusExtensions { + public static Double getCpuUsagePercent(Status status) { + return status.getCpuInfo() != null ? status.getCpuInfo().getUsagePercent() : null; + } + + public static Double getMemoryUsagePercent(Status status) { + return status.getMemoryInfo() != null ? status.getMemoryInfo().getUsagePercent() : null; + } + } + + private double calculateThreshold(List statuses, java.util.function.Function getter) { + // 收集所有有效数值 + List values = statuses.stream() + .map(getter) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (values.size() < 2) { + return 0.0; + } + + // 计算相邻数值间的差值 + List differences = new ArrayList<>(); + for (int i = 1; i < values.size(); i++) { + differences.add(Math.abs(values.get(i) - values.get(i-1))); + } + + // 计算平均差值作为阈值基础 + double averageDifference = differences.stream() + .mapToDouble(Double::doubleValue) + .average() + .orElse(0.0); + + // 返回平均差值的一半作为阈值,这样可以过滤掉较小的变化 + return averageDifference / 2; + } + } diff --git a/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/been/Status.java b/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/been/Status.java index 1f52c25..97eac02 100644 --- a/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/been/Status.java +++ b/reisa-admin/reisaAdminSpring/src/main/java/org/ast/reisaadminspring/been/Status.java @@ -147,6 +147,20 @@ public class Status { this.error = error; } + public Double getCpuUsagePercent() { + if (cpuInfo == null) { + return 0.0; + } + return cpuInfo.getUsagePercent(); + } + + public Double getMemoryUsagePercent() { + if (memoryInfo == null) { + return 0.0; + } + return memoryInfo.getUsagePercent(); + } + // 内部类定义 public static class CpuInfo { private String modelName; diff --git a/reisa-admin/reisaAdminSpring/src/main/resources/application-dev.properties b/reisa-admin/reisaAdminSpring/src/main/resources/application-dev.properties new file mode 100644 index 0000000..ae86713 --- /dev/null +++ b/reisa-admin/reisaAdminSpring/src/main/resources/application-dev.properties @@ -0,0 +1,6 @@ +spring.application.name=reisaAdminSpring +spring.data.mongodb.uri=mongodb://reisaAdmin:nbAC8hi8xdJeBDDT@127.0.0.1:27017/reisaadmin +server.port=48102 + +spring.data.redis.host=127.0.0.1 +spring.data.redis.port: 6379 \ No newline at end of file diff --git a/reisa-admin/src/App.vue b/reisa-admin/src/App.vue index 9f5f1df..7f9d738 100644 --- a/reisa-admin/src/App.vue +++ b/reisa-admin/src/App.vue @@ -497,13 +497,22 @@ v-model:value="selectedMonitorServers" mode="multiple" placeholder="请选择要监控的服务器" - style="width: 800px; margin-right: 16px;" + style="width: 600px; margin-right: 16px;" @change="loadMonitorData" > {{ server.name }} ({{ server.ipAddress }}) + + 刷新数据 + 关闭监控 +
+ +
@@ -532,11 +550,11 @@

进程排行

- + 按CPU排序 按内存排序 - + 降序 升序 @@ -572,7 +590,7 @@
- + @@ -659,6 +677,7 @@ import {ref, reactive, onMounted, computed, onUnmounted, nextTick} from 'vue'; import { message } from 'ant-design-vue'; import axios from 'axios'; import { PlusOutlined, CopyOutlined } from '@ant-design/icons-vue'; +import dayjs from 'dayjs'; // 状态管理 const servers = ref([]); @@ -673,6 +692,7 @@ const sortField = ref('name'); const sortOrder = ref('asc'); const localSearchName = ref(''); const showMonitor = ref(true); +const showChartPoints = ref(false); const deviceInfoModalVisible = ref(false); document.title = 'ReisaAdmin'; @@ -851,6 +871,7 @@ const columns = [ // 初始化加载数据 onMounted(() => { + openMonitor() fetchServers(); }); @@ -1156,11 +1177,14 @@ const closeDeviceInfoModal = () => { deviceInfoModalVisible.value = false; stopDeviceInfoRefreshTimer(); }; + // 大屏监控相关状态 const monitorVisible = ref(false); const selectedMonitorServers = ref([]); // 多选的服务器 const monitorHistoryData = ref({}); // 存储各服务器的历史数据 const monitorLoading = ref(false); +const monitorRefreshTimer = ref(null); // 监控数据自动刷新定时器 +const monitorTimeRange = ref([]); // 监控时间范围 // 图表实例引用 const cpuChartRef = ref(null); @@ -1196,15 +1220,45 @@ const sortProcesses = () => { // 打开监控大屏 const openMonitor = () => { - monitorVisible.value = true; + showMonitor.value = true; selectedMonitorServers.value = []; monitorHistoryData.value = {}; + // 设置默认时间范围为最近1小时 + const endTime = dayjs(); + const startTime = endTime.subtract(1, 'hour'); + monitorTimeRange.value = [startTime, endTime]; + startMonitorRefreshTimer(); }; -// 获取服务器历史数据 +// 处理时间范围变化 +const handleTimeRangeChange = () => { + if (selectedMonitorServers.value.length > 0) { + loadMonitorData(); + } +}; +const formatTime = (timestamp) => { + if (!timestamp) return 'N/A'; + const date = new Date(timestamp); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${month}-${day} ${hours}:${minutes}`; +}; + +// 获取服务器历史数据(修改后的版本,支持时间范围) const fetchServerHistory = async (serverIp) => { try { - const response = await axios.get(`/api/v1/status/history/${serverIp}?limit=50`); + const params = {}; + + // 如果设置了时间范围,则添加时间参数 + if (monitorTimeRange.value && monitorTimeRange.value.length === 2) { + const [startTime, endTime] = monitorTimeRange.value; + params.startDate = startTime.format('YYYY-MM-DD HH:mm:ss'); + params.endDate = endTime.format('YYYY-MM-DD HH:mm:ss'); + } + + const response = await axios.get(`/api/v1/status/history/${serverIp}`, { params }); return response.data; } catch (error) { message.error(`获取服务器 ${serverIp} 历史数据失败: ${error.message}`); @@ -1212,9 +1266,8 @@ const fetchServerHistory = async (serverIp) => { } }; -// 加载选中服务器的历史数据 +// 修改 loadMonitorData 函数以使用新的多IP API const loadMonitorData = async () => { - allProcesses.value = []; if (selectedMonitorServers.value.length === 0) { monitorHistoryData.value = {}; allProcesses.value = []; @@ -1224,42 +1277,71 @@ const loadMonitorData = async () => { monitorLoading.value = true; try { - const promises = selectedMonitorServers.value.map(serverId => { - const server = servers.value.find(s => s.id === serverId); - return server ? fetchServerHistory(server.ipAddress) : Promise.resolve([]); - }); + // 限制同时监控的服务器数量 + const limitedServers = selectedMonitorServers.value.slice(0, 99999); - const results = await Promise.all(promises); + // 获取选中服务器的IP地址 + const serverIps = limitedServers + .map(serverId => { + const server = servers.value.find(s => s.id === serverId); + return server ? server.ipAddress : null; + }) + .filter(Boolean); // 过滤掉null值 + + if (serverIps.length === 0) { + monitorHistoryData.value = {}; + allProcesses.value = []; + drawCharts(); + return; + } + + // 构造请求参数 + const params = {}; + if (monitorTimeRange.value && monitorTimeRange.value.length === 2) { + const [startTime, endTime] = monitorTimeRange.value; + params.startDate = startTime.format('YYYY-MM-DD HH:mm:ss'); + params.endDate = endTime.format('YYYY-MM-DD HH:mm:ss'); + } + + // 使用新的多IP API接口 + const response = await axios.get(`/api/v1/status/history/all/${serverIps.join(',')}`, { params }); + const results = response.data; // 整理数据 const newData = {}; const processes = []; - selectedMonitorServers.value.forEach((serverId, index) => { + + limitedServers.forEach((serverId, index) => { const server = servers.value.find(s => s.id === serverId); - if (server) { + if (server && results[server.ipAddress]) { newData[serverId] = { serverName: server.name, serverIp: server.ipAddress, - history: results[index] + history: results[server.ipAddress] }; - // 收集所有进程数据 - results[index].forEach(historyItem => { - if (historyItem.processes) { - historyItem.processes.forEach(process => { - processes.push({ - ...process, - serverName: server.name, - serverId: serverId - }); - }); + // 限制每个服务器的进程数据数量 + let serverProcesses = []; + if (results[server.ipAddress].length > 0) { + const latestData = results[server.ipAddress][0]; + if (latestData.processes) { + // 只取前50个进程 + serverProcesses = latestData.processes.slice(0, 50); } + } + + serverProcesses.forEach(process => { + processes.push({ + ...process, + serverName: server.name, + serverId: serverId + }); }); } }); monitorHistoryData.value = newData; - allProcesses.value = processes; + allProcesses.value = processes.slice(0, 100); // 限制总进程数 drawCharts(); } catch (error) { message.error('加载监控数据失败: ' + error.message); @@ -1268,6 +1350,7 @@ const loadMonitorData = async () => { } }; + // 绘制图表 const drawCharts = () => { // 确保DOM已更新 @@ -1276,11 +1359,29 @@ const drawCharts = () => { drawMemoryChart(); }); }; + import { Chart, registerables } from 'chart.js'; import 'chartjs-adapter-date-fns'; // 注册Chart.js的所有组件 Chart.register(...registerables); -// 绘制CPU使用率图表 + +// 数据采样函数 - 当数据点过多时减少数据点数量 +const sampleData = (data, maxPoints = 50) => { + if (data.length <= maxPoints) { + return data; + } + + const sampled = []; + const step = Math.ceil(data.length / maxPoints); + + for (let i = 0; i < data.length; i += step) { + sampled.push(data[i]); + } + + return sampled; +}; + +// 修改 drawCPUChart 函数 const drawCPUChart = () => { if (!cpuChartRef.value) return; @@ -1295,18 +1396,25 @@ const drawCPUChart = () => { const datasets = []; Object.keys(monitorHistoryData.value).forEach(serverId => { const serverData = monitorHistoryData.value[serverId]; - const data = serverData.history.map(item => ({ + let data = serverData.history.map(item => ({ x: new Date(item.timestamp), y: item.cpuInfo?.usagePercent || 0 })).reverse(); // 图表从左到右时间顺序 + // 当数据量过大时进行采样处理 + if (data.length > 50) { + data = sampleData(data, 50); + } + datasets.push({ label: serverData.serverName, data: data, borderColor: getColorByIndex(datasets.length), backgroundColor: getColorByIndex(datasets.length, 0.1), - tension: 0.1, - fill: false + tension: 0.4, // 增加曲线平滑度 + fill: true, + pointRadius: showChartPoints.value ? 3 : 0, // 根据开关控制点的显示 + pointHoverRadius: showChartPoints.value ? 5 : 0 // 悬停时点的大小 }); }); @@ -1318,13 +1426,14 @@ const drawCPUChart = () => { options: { responsive: true, maintainAspectRatio: false, + animation: false, // 禁用动画以提高性能 scales: { x: { type: 'time', time: { unit: 'minute', displayFormats: { - minute: 'HH:mm' + minute: 'MM-dd HH:mm' } }, title: { @@ -1355,7 +1464,7 @@ const drawCPUChart = () => { }); }; -// 绘制内存使用率图表 +// 同样需要修改 drawMemoryChart 函数中的点显示逻辑 const drawMemoryChart = () => { if (!memoryChartRef.value) return; @@ -1370,18 +1479,25 @@ const drawMemoryChart = () => { const datasets = []; Object.keys(monitorHistoryData.value).forEach(serverId => { const serverData = monitorHistoryData.value[serverId]; - const data = serverData.history.map(item => ({ + let data = serverData.history.map(item => ({ x: new Date(item.timestamp), y: item.memoryInfo?.usagePercent || 0 })).reverse(); + // 当数据量过大时进行采样处理 + if (data.length > 50) { + data = sampleData(data, 50); + } + datasets.push({ label: serverData.serverName, data: data, borderColor: getColorByIndex(datasets.length), backgroundColor: getColorByIndex(datasets.length, 0.1), - tension: 0.1, - fill: false + tension: 0.4, // 增加曲线平滑度 + fill: true, + pointRadius: showChartPoints.value ? 3 : 0, // 根据开关控制点的显示 + pointHoverRadius: showChartPoints.value ? 5 : 0 // 悬停时点的大小 }); }); @@ -1393,13 +1509,14 @@ const drawMemoryChart = () => { options: { responsive: true, maintainAspectRatio: false, + animation: false, // 禁用动画以提高性能 scales: { x: { type: 'time', time: { unit: 'minute', displayFormats: { - minute: 'HH:mm' + minute: 'MM-dd HH:mm' } }, title: { @@ -1445,7 +1562,7 @@ const getColorByIndex = (index, alpha = 1) => { // 关闭监控大屏 const closeMonitor = () => { - monitorVisible.value = false; + showMonitor.value = false; // 销毁图表实例 if (cpuChartInstance) { cpuChartInstance.destroy(); @@ -1455,12 +1572,35 @@ const closeMonitor = () => { memoryChartInstance.destroy(); memoryChartInstance = null; } + stopMonitorRefreshTimer(); +}; + +// 添加启动监控自动刷新函数 +const startMonitorRefreshTimer = () => { + // 先清除已存在的定时器 + if (monitorRefreshTimer.value) { + clearInterval(monitorRefreshTimer.value); + } + + // 启动新的定时器,每30秒刷新一次监控数据 + monitorRefreshTimer.value = setInterval(() => { + if (showMonitor.value && selectedMonitorServers.value.length > 0) { + loadMonitorData(); + } + }, 30000); // 30秒刷新一次 +}; + +// 添加停止监控自动刷新函数 +const stopMonitorRefreshTimer = () => { + if (monitorRefreshTimer.value) { + clearInterval(monitorRefreshTimer.value); + monitorRefreshTimer.value = null; + } }; // 在组件卸载时清理定时器 onUnmounted(() => { closeMonitor(); - stopDeviceInfoRefreshTimer(); }); @@ -1531,7 +1671,10 @@ onUnmounted(() => { color: #1d2129; margin: 0; } - +.chart-controls { + margin-bottom: 16px; + text-align: right; +} .add-button { border-radius: 6px; height: 40px;