From b6a58b737693228c83b8f95b65f50b38a5a1ece7 Mon Sep 17 00:00:00 2001 From: Timi Date: Tue, 7 Apr 2026 20:12:52 +0800 Subject: [PATCH] update system status api, add UPS/Docker status api --- .../system/bean/DockerStatusStore.java | 142 ++++++ .../api/modules/system/bean/ServerStatus.java | 251 ++++++++--- .../modules/system/bean/UpsStatusStore.java | 169 +++++++ .../system/controller/DockerController.java | 43 ++ .../system/controller/SystemController.java | 50 ++- .../system/controller/UpsController.java | 34 ++ .../modules/system/service/DockerService.java | 40 ++ .../modules/system/service/StatusService.java | 30 ++ .../modules/system/service/UpsService.java | 28 ++ .../implement/DockerServiceImplement.java | 171 ++++++++ .../implement/StatusServiceImplement.java | 414 ++++++++++++++++++ .../implement/UpsServiceImplement.java | 151 +++++++ .../modules/system/task/DockerStatusTask.java | 272 ++++++++++++ .../modules/system/task/ServerStatusTask.java | 233 ++-------- .../modules/system/task/UpsStatusTask.java | 89 ++++ .../status/AbstractDequeStatusCollector.java | 29 ++ .../task/status/StatusCollectContext.java | 37 ++ .../system/task/status/StatusCollector.java | 25 ++ .../status/collector/CpuStatusCollector.java | 50 +++ .../collector/HardwareStatusCollector.java | 46 ++ .../status/collector/JvmStatusCollector.java | 100 +++++ .../collector/MemoryStatusCollector.java | 29 ++ .../collector/NetworkStatusCollector.java | 80 ++++ .../status/collector/OSStatusCollector.java | 28 ++ .../collector/StorageStatusCollector.java | 90 ++++ .../system/util/DockerEngineClient.java | 274 ++++++++++++ .../system/util/SystemAPIInterceptor.java | 4 +- .../modules/system/util/UpsStatusClient.java | 265 +++++++++++ .../system/vo/SystemStatusDataView.java | 381 ++++++++++++++++ .../system/vo/SystemStatusHistoryView.java | 31 ++ .../system/vo/SystemStatusSnapshotView.java | 22 + .../DockerContainerHistoryPointView.java | 40 ++ .../vo/docker/DockerContainerHistoryView.java | 37 ++ .../vo/docker/DockerContainerStatusView.java | 82 ++++ .../vo/docker/DockerContainerSummaryView.java | 52 +++ .../system/vo/ups/UpsHistoryPointView.java | 58 +++ .../modules/system/vo/ups/UpsHistoryView.java | 31 ++ .../modules/system/vo/ups/UpsStatusView.java | 94 ++++ 38 files changed, 3721 insertions(+), 281 deletions(-) create mode 100644 src/main/java/com/imyeyu/api/modules/system/bean/DockerStatusStore.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/bean/UpsStatusStore.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/controller/DockerController.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/controller/UpsController.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/service/DockerService.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/service/StatusService.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/service/UpsService.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/service/implement/DockerServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/service/implement/UpsServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/DockerStatusTask.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/UpsStatusTask.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollectContext.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/CpuStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/HardwareStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/JvmStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/MemoryStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/OSStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/task/status/collector/StorageStatusCollector.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/util/DockerEngineClient.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/util/UpsStatusClient.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusDataView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusHistoryView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusSnapshotView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryPointView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerStatusView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerSummaryView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryPointView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryView.java create mode 100644 src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsStatusView.java diff --git a/src/main/java/com/imyeyu/api/modules/system/bean/DockerStatusStore.java b/src/main/java/com/imyeyu/api/modules/system/bean/DockerStatusStore.java new file mode 100644 index 0000000..d39d2cc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/bean/DockerStatusStore.java @@ -0,0 +1,142 @@ +package com.imyeyu.api.modules.system.bean; + +import lombok.Data; +import org.springframework.stereotype.Component; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Docker 容器状态缓存 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +@Component +public class DockerStatusStore { + + /** 容器状态映射 */ + private final Map containers = new LinkedHashMap<>(); + + /** + * 容器缓存 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Container { + + /** 容器 ID */ + private String id; + + /** 容器名称 */ + private String name; + + /** 镜像 */ + private String image; + + /** 镜像 ID */ + private String imageId; + + /** 创建时间 */ + private long createdAt; + + /** 运行状态 */ + private String state; + + /** 状态描述 */ + private String status; + + /** 健康状态 */ + private String healthStatus; + + /** 启动时间 */ + private String startedAt; + + /** 结束时间 */ + private String finishedAt; + + /** 退出码 */ + private Integer exitCode; + + /** 重启次数 */ + private int restartCount; + + /** OOM 标记 */ + private boolean oomKilled; + + /** CPU 百分比 */ + private Double cpuPercent; + + /** 内存使用量 */ + private Long memoryUsageBytes; + + /** 内存限制 */ + private Long memoryLimitBytes; + + /** 内存百分比 */ + private Double memoryPercent; + + /** 网络接收字节 */ + private Long networkRxBytes; + + /** 网络发送字节 */ + private Long networkTxBytes; + + /** 块设备读取字节 */ + private Long blockReadBytes; + + /** 块设备写入字节 */ + private Long blockWriteBytes; + + /** 进程数 */ + private Integer pids; + + /** 采样时间 */ + private long updatedAt; + + /** 历史点 */ + private final Deque history = new ArrayDeque<>(); + } + + /** + * 容器指标历史点 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Point { + + /** 时间 */ + private long at; + + /** CPU 百分比 */ + private Double cpuPercent; + + /** 内存使用量 */ + private Long memoryUsageBytes; + + /** 内存百分比 */ + private Double memoryPercent; + + /** 网络接收字节 */ + private Long networkRxBytes; + + /** 网络发送字节 */ + private Long networkTxBytes; + + /** 块设备读取字节 */ + private Long blockReadBytes; + + /** 块设备写入字节 */ + private Long blockWriteBytes; + + /** 进程数 */ + private Integer pids; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/bean/ServerStatus.java b/src/main/java/com/imyeyu/api/modules/system/bean/ServerStatus.java index 55b3a0e..8266f17 100644 --- a/src/main/java/com/imyeyu/api/modules/system/bean/ServerStatus.java +++ b/src/main/java/com/imyeyu/api/modules/system/bean/ServerStatus.java @@ -1,7 +1,7 @@ package com.imyeyu.api.modules.system.bean; -import lombok.Data; import com.imyeyu.java.TimiJava; +import lombok.Data; import org.springframework.stereotype.Component; import java.util.ArrayDeque; @@ -11,22 +11,24 @@ import java.util.LinkedList; import java.util.List; /** - * 服务器状态数据,所有动态数据左出右进,此对象由 IOC 托管 + * 服务端状态缓存 + * + *

该对象只用于采集任务内部缓存,不直接作为接口协议返回。

* * @author 夜雨 - * @version 2022-01-31 15:35 + * @since 2022-01-31 15:35 */ @Data @Component public class ServerStatus implements TimiJava { - /** 动态数据更新时轴 */ + /** 采样时间轴 */ private LinkedList updateAxis = new LinkedList<>(); - /** 系统 */ + /** 操作系统 */ private OS os = new OS(); - /** CPU 使用率 */ + /** CPU */ private CPU cpu = new CPU(); /** 系统内存 */ @@ -35,17 +37,20 @@ public class ServerStatus implements TimiJava { /** 网络 */ private Network network = new Network(); - /** 本程序状态 */ + /** 硬件 */ + private Hardware hardware = new Hardware(); + + /** JVM */ private JVM jvm = new JVM(); - /** 磁盘 */ - private List partitions = new ArrayList<>(); + /** 存储分区 */ + private List storagePartitions = new ArrayList<>(); /** - * 系统 + * 操作系统 * * @author 夜雨 - * @version 2022-08-12 20:55 + * @since 2022-08-12 20:55 */ @Data public static class OS { @@ -58,10 +63,10 @@ public class ServerStatus implements TimiJava { } /** - * 虚拟机状态 + * JVM * * @author 夜雨 - * @version 2022-01-31 21:10 + * @since 2022-01-31 21:10 */ @Data public static class JVM { @@ -69,80 +74,83 @@ public class ServerStatus implements TimiJava { /** 启动时间 */ private long bootAt; - /** JVM 名称 */ + /** 名称 */ private String name; - /** JVM 版本 */ + /** 版本 */ private String version; - /** 内存 */ + /** GC 名称 */ + private String gcName; + + /** 堆内存 */ private Memory memory = new Memory(); - /** 内存回收 */ - private ZGC zgc = new ZGC(); + /** GC 状态 */ + private GC gc = new GC(); /** - * 内存 + * JVM 内存 * * @author 夜雨 - * @version 2022-08-12 20:32 + * @since 2022-08-12 20:32 */ @Data public static class Memory { - /** 初始化 */ + /** 初始大小 */ private long init; - /** 最大 */ + /** 最大大小 */ private long max; - /** 已使用 */ + /** 已用大小 */ private final Deque used = new ArrayDeque<>(); - /** 已提交 */ + /** 已提交大小 */ private final Deque committed = new ArrayDeque<>(); } /** - * 内存回收 + * GC 状态 * * @author 夜雨 - * @version 2022-08-12 20:32 + * @since 2022-08-12 20:32 */ @Data - public static class ZGC { + public static class GC { - /** 并发周期 */ - private long syncCycles = 0; + /** 周期次数 */ + private long syncCycles; - /** 累计并发周期耗时(毫秒) */ - private long syncCyclesTimeTotal = 0; + /** 周期累计耗时 */ + private long syncCyclesTimeTotal; - /** 累计次数 */ - private long pauses = 0; + /** 暂停次数 */ + private long pauses; - /** 累计回收暂停时长(毫秒) */ - private long pausesTimeTotal = 0; + /** 暂停累计耗时 */ + private long pausesTimeTotal; - /** 上一次回收时间 */ - private long lastPauseAt = 0; + /** 上次暂停时间 */ + private long lastPauseAt; - /** 上一次回收大小 */ - private long lastRecoverySize = 0; + /** 上次回收大小 */ + private long lastRecoverySize; - /** 并发周期耗时 */ + /** 周期耗时序列 */ private final Deque syncCyclesTime = new ArrayDeque<>(); - /** 回收暂停时长 */ + /** 暂停耗时序列 */ private final Deque pausesTime = new ArrayDeque<>(); } } /** - * 中央处理器 + * CPU * * @author 夜雨 - * @version 2022-01-31 15:40 + * @since 2022-01-31 15:40 */ @Data public static class CPU { @@ -150,19 +158,19 @@ public class ServerStatus implements TimiJava { /** 名称 */ private String name; - /** 物理核心数量 */ + /** 物理核心数 */ private int coreCount; - /** 线程数量 */ + /** 逻辑核心数 */ private int logicalCount; /** 温度 */ private double temperature; - /** 系统使用 */ + /** 系统占用 */ private final Deque system = new ArrayDeque<>(); - /** 总共已使用 */ + /** 总占用 */ private final Deque used = new ArrayDeque<>(); } @@ -170,7 +178,7 @@ public class ServerStatus implements TimiJava { * 系统内存 * * @author 夜雨 - * @version 2022-01-31 15:50 + * @since 2022-01-31 15:50 */ @Data public static class Memory { @@ -178,69 +186,174 @@ public class ServerStatus implements TimiJava { /** 物理内存大小 */ private long size; - /** 交换区大小 */ + /** 交换分区大小 */ private long swapSize; - /** 已使用 */ + /** 已用内存 */ private final Deque used = new ArrayDeque<>(); - /** 交换区已使用 */ + /** 已用交换分区 */ private final Deque swapUsed = new ArrayDeque<>(); } /** - * 网卡网速 + * 网络 * * @author 夜雨 - * @version 2022-08-10 21:41 + * @since 2022-08-10 21:41 */ @Data public static class Network { + /** 网卡名称 */ + private String name; + /** 累计接收 */ private long recvTotal; /** 累计发送 */ private long sentTotal; - /** 实时接收速度 */ + /** 实时接收速率 */ private long recvNow; - /** 实时发送速度 */ + /** 实时发送速率 */ private long sentNow; /** MAC 地址 */ private String mac; - /** 发送 */ + /** 接收包总数 */ + private long recvPacketsTotal; + + /** 发送包总数 */ + private long sentPacketsTotal; + + /** 输入错误包总数 */ + private long inErrors; + + /** 输出错误包总数 */ + private long outErrors; + + /** 输入丢弃包总数 */ + private long inDrops; + + /** 碰撞总数 */ + private long collisions; + + /** 发送序列 */ private final Deque sent = new ArrayDeque<>(); - /** 接收 */ + /** 接收序列 */ private final Deque recv = new ArrayDeque<>(); } /** - * 分区 + * 硬件信息 * * @author 夜雨 - * @version 2022-01-31 20:19 + * @since 2026-04-06 */ @Data - public static class Partition { + public static class Hardware { - /** 识别 UUID */ + /** 风扇转速 */ + private List fanSpeeds = new ArrayList<>(); + + /** 主板信息 */ + private Baseboard baseboard = new Baseboard(); + + /** BIOS 信息 */ + private Firmware firmware = new Firmware(); + } + + /** + * 主板信息 + * + * @author 夜雨 + * @since 2026-04-06 + */ + @Data + public static class Baseboard { + + /** 厂商 */ + private String manufacturer; + + /** 型号 */ + private String model; + + /** 版本 */ + private String version; + + /** 序列号 */ + private String serialNumber; + } + + /** + * BIOS 信息 + * + * @author 夜雨 + * @since 2026-04-06 + */ + @Data + public static class Firmware { + + /** 厂商 */ + private String manufacturer; + + /** 名称 */ + private String name; + + /** 描述 */ + private String description; + + /** 版本 */ + private String version; + + /** 发布时间 */ + private String releaseDate; + } + + /** + * 存储分区 + * + * @author 夜雨 + * @since 2026-04-06 + */ + @Data + public static class StoragePartition { + + /** 物理磁盘名称 */ + private String diskName; + + /** 物理磁盘型号 */ + private String diskModel; + + /** 物理磁盘序列号 */ + private String diskSerial; + + /** 分区标识 */ + private String partitionId; + + /** 分区名称 */ + private String partitionName; + + /** 分区类型 */ + private String partitionType; + + /** 分区 UUID */ private String uuid; - /** 路径 */ - private String path; + /** 挂载点 */ + private String mountPoint; - /** 文件系统类型 */ - private String type; + /** 分区总空间 */ + private long totalBytes; - /** 已使用 */ - private long used; + /** 分区已用空间 */ + private Long usedBytes; - /** 总大小 */ - private long total; + /** 磁盘传输耗时 */ + private long transferTimeMs; } } diff --git a/src/main/java/com/imyeyu/api/modules/system/bean/UpsStatusStore.java b/src/main/java/com/imyeyu/api/modules/system/bean/UpsStatusStore.java new file mode 100644 index 0000000..2b0733d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/bean/UpsStatusStore.java @@ -0,0 +1,169 @@ +package com.imyeyu.api.modules.system.bean; + +import lombok.Data; +import org.springframework.stereotype.Component; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +/** + * UPS 状态缓存 + * + * @author Codex + * @since 2026-04-07 + */ +@Data +@Component +public class UpsStatusStore { + + /** 当前状态 */ + private Snapshot current; + + /** 历史点 */ + private final Deque history = new ArrayDeque<>(); + + /** + * UPS 当前快照 + * + * @author Codex + * @since 2026-04-07 + */ + @Data + public static class Snapshot { + + /** 采样时间 */ + private long updatedAt; + + /** UPS 数据时间 */ + private Long upsTime; + + /** 上游主机地址 */ + private String hostName; + + /** 厂商名称 */ + private String customer; + + /** 上游版本 */ + private String version; + + /** 设备标识 */ + private String deviceId; + + /** UPS 类型 */ + private String upsType; + + /** UPS 形态 */ + private String morphological; + + /** 输入输出相位 */ + private String ioPhase; + + /** 工作模式 */ + private String workMode; + + /** 输入电压 */ + private Double inputVoltage; + + /** 输入频率 */ + private Double inputFrequency; + + /** 输出电压 */ + private Double outputVoltage; + + /** 输出频率 */ + private Double outputFrequency; + + /** 输出负载百分比 */ + private Integer outputLoadPercent; + + /** 电池电压 */ + private Double batteryVoltage; + + /** 电池容量百分比 */ + private Integer batteryCapacity; + + /** 电池剩余时间 */ + private Integer batteryRemainTime; + + /** 温度 */ + private Double temperature; + + /** 是否旁路运行 */ + private boolean bypassActive; + + /** 是否关机中 */ + private boolean shutdownActive; + + /** 是否输出开启 */ + private boolean outputOn; + + /** 是否充电中 */ + private boolean charging; + + /** 故障类型 */ + private String faultType; + + /** 故障明细 */ + private String faultKind; + + /** 告警列表 */ + private List warnings = new ArrayList<>(); + } + + /** + * UPS 历史点 + * + * @author Codex + * @since 2026-04-07 + */ + @Data + public static class Point { + + /** 采样时间 */ + private long at; + + /** UPS 数据时间 */ + private Long upsTime; + + /** 工作模式 */ + private String workMode; + + /** 输入电压 */ + private Double inputVoltage; + + /** 输入频率 */ + private Double inputFrequency; + + /** 输出电压 */ + private Double outputVoltage; + + /** 输出频率 */ + private Double outputFrequency; + + /** 输出负载百分比 */ + private Integer outputLoadPercent; + + /** 电池电压 */ + private Double batteryVoltage; + + /** 电池容量百分比 */ + private Integer batteryCapacity; + + /** 电池剩余时间 */ + private Integer batteryRemainTime; + + /** 温度 */ + private Double temperature; + + /** 是否旁路运行 */ + private boolean bypassActive; + + /** 是否关机中 */ + private boolean shutdownActive; + + /** 是否输出开启 */ + private boolean outputOn; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/controller/DockerController.java b/src/main/java/com/imyeyu/api/modules/system/controller/DockerController.java new file mode 100644 index 0000000..31b7c73 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/controller/DockerController.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.system.controller; + +import com.imyeyu.api.modules.system.service.DockerService; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Docker 控制器 + * + * @author Codex + * @since 2026-04-06 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/docker") +public class DockerController { + + private final DockerService dockerService; + + @GetMapping("/containers") + public List listContainers() { + return dockerService.listContainers(); + } + + @GetMapping("/containers/{containerId}/status") + public DockerContainerStatusView getContainerStatus(@PathVariable String containerId) { + return dockerService.getContainerStatus(containerId); + } + + @GetMapping("/containers/{containerId}/history") + public DockerContainerHistoryView getContainerHistory(@PathVariable String containerId, @RequestParam(required = false) String window) { + return dockerService.getContainerHistory(containerId, window); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/controller/SystemController.java b/src/main/java/com/imyeyu/api/modules/system/controller/SystemController.java index 03fdaa4..c82b25b 100644 --- a/src/main/java/com/imyeyu/api/modules/system/controller/SystemController.java +++ b/src/main/java/com/imyeyu/api/modules/system/controller/SystemController.java @@ -2,8 +2,10 @@ package com.imyeyu.api.modules.system.controller; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.service.AttachmentService; -import com.imyeyu.api.modules.system.bean.ServerStatus; +import com.imyeyu.api.modules.system.service.StatusService; import com.imyeyu.api.modules.system.service.SystemService; +import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView; +import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView; import com.imyeyu.api.modules.system.vo.TempAttachRequest; import com.imyeyu.io.IO; import com.imyeyu.java.bean.timi.TimiCode; @@ -12,6 +14,7 @@ import com.imyeyu.spring.annotation.AOPLog; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -23,10 +26,10 @@ import java.io.IOException; import java.util.concurrent.Semaphore; /** - * 服务器控制接口 + * 服务端控制器 * * @author 夜雨 - * @version 2022-01-31 22:47 + * @since 2022-01-31 22:47 */ @Slf4j @RestController @@ -34,7 +37,7 @@ import java.util.concurrent.Semaphore; @RequestMapping("/system/server") public class SystemController { - private final ServerStatus serverStatus; + private final StatusService statusService; private final SystemService service; private final AttachmentService attachmentService; @@ -42,16 +45,36 @@ public class SystemController { private final Semaphore rebootSemaphore = new Semaphore(1); private final Semaphore restoreSemaphore = new Semaphore(1); - /** @return 实时服务器状态 */ - @RequestMapping("/status") - public ServerStatus getStatus() { - return serverStatus; + /** + * 获取系统状态快照 + * + * @param metrics 返回指标,使用逗号分隔 + * @return 状态快照 + */ + @GetMapping("/status") + public SystemStatusSnapshotView getStatus(@RequestParam(required = false) String metrics) { + return statusService.getStatus(metrics); + } + + /** + * 获取系统状态历史 + * + * @param window 历史窗口,支持 s/m/h/d 后缀 + * @param metrics 返回指标,使用逗号分隔 + * @return 状态历史 + */ + @GetMapping("/status/history") + public SystemStatusHistoryView getStatusHistory( + @RequestParam(required = false) String window, + @RequestParam(required = false) String metrics + ) { + return statusService.getStatusHistory(window, metrics); } /** * 更新系统 * - * @param file + * @param file 更新文件 */ @AOPLog @PostMapping("/update") @@ -85,8 +108,7 @@ public class SystemController { } /** - * 停止系统 - * + * 关闭系统 */ @AOPLog @RequestMapping("/shutdown") @@ -111,7 +133,11 @@ public class SystemController { } } - // TODO 临时接口 + /** + * 上传临时附件 + * + * @param request 上传请求 + */ @AOPLog @PostMapping("/attach") public void uploadAttachment(TempAttachRequest request) { diff --git a/src/main/java/com/imyeyu/api/modules/system/controller/UpsController.java b/src/main/java/com/imyeyu/api/modules/system/controller/UpsController.java new file mode 100644 index 0000000..46c1495 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/controller/UpsController.java @@ -0,0 +1,34 @@ +package com.imyeyu.api.modules.system.controller; + +import com.imyeyu.api.modules.system.service.UpsService; +import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView; +import com.imyeyu.api.modules.system.vo.ups.UpsStatusView; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * UPS 控制器 + * + * @author Codex + * @since 2026-04-07 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/ups") +public class UpsController { + + private final UpsService upsService; + + @GetMapping("/status") + public UpsStatusView getStatus() { + return upsService.getStatus(); + } + + @GetMapping("/history") + public UpsHistoryView getHistory(@RequestParam(required = false) String window) { + return upsService.getHistory(window); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/DockerService.java b/src/main/java/com/imyeyu/api/modules/system/service/DockerService.java new file mode 100644 index 0000000..16ab90f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/service/DockerService.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.system.service; + +import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView; + +import java.util.List; + +/** + * Docker 查询服务 + * + * @author Codex + * @since 2026-04-06 + */ +public interface DockerService { + + /** + * 获取容器列表 + * + * @return 容器列表 + */ + List listContainers(); + + /** + * 获取容器状态 + * + * @param containerId 容器 ID + * @return 容器状态 + */ + DockerContainerStatusView getContainerStatus(String containerId); + + /** + * 获取容器历史 + * + * @param containerId 容器 ID + * @param window 历史窗口 + * @return 容器历史 + */ + DockerContainerHistoryView getContainerHistory(String containerId, String window); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/StatusService.java b/src/main/java/com/imyeyu/api/modules/system/service/StatusService.java new file mode 100644 index 0000000..d95f3d6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/service/StatusService.java @@ -0,0 +1,30 @@ +package com.imyeyu.api.modules.system.service; + +import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView; +import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView; + +/** + * 系统状态查询服务 + * + * @author Codex + * @since 2026-04-06 + */ +public interface StatusService { + + /** + * 获取当前状态快照 + * + * @param metrics 返回指标 + * @return 状态快照 + */ + SystemStatusSnapshotView getStatus(String metrics); + + /** + * 获取状态历史 + * + * @param window 历史窗口 + * @param metrics 返回指标 + * @return 状态历史 + */ + SystemStatusHistoryView getStatusHistory(String window, String metrics); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/UpsService.java b/src/main/java/com/imyeyu/api/modules/system/service/UpsService.java new file mode 100644 index 0000000..6f47e15 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/service/UpsService.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.system.service; + +import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView; +import com.imyeyu.api.modules.system.vo.ups.UpsStatusView; + +/** + * UPS 查询服务 + * + * @author Codex + * @since 2026-04-07 + */ +public interface UpsService { + + /** + * 获取 UPS 当前状态 + * + * @return UPS 状态 + */ + UpsStatusView getStatus(); + + /** + * 获取 UPS 历史 + * + * @param window 历史窗口 + * @return UPS 历史 + */ + UpsHistoryView getHistory(String window); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/DockerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/DockerServiceImplement.java new file mode 100644 index 0000000..7aa35ec --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/DockerServiceImplement.java @@ -0,0 +1,171 @@ +package com.imyeyu.api.modules.system.service.implement; + +import com.imyeyu.api.modules.system.bean.DockerStatusStore; +import com.imyeyu.api.modules.system.service.DockerService; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryPointView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView; +import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * Docker 查询服务实现 + * + * @author Codex + * @since 2026-04-06 + */ +@Service +@RequiredArgsConstructor +public class DockerServiceImplement implements DockerService { + + private final DockerStatusStore dockerStatusStore; + + @Value("${docker.engine.collect-rate-ms:10000}") + private long collectRateMs; + + @Override + public List listContainers() { + synchronized (dockerStatusStore) { + List result = new ArrayList<>(dockerStatusStore.getContainers().size()); + for (DockerStatusStore.Container container : dockerStatusStore.getContainers().values()) { + DockerContainerSummaryView item = new DockerContainerSummaryView(); + item.setId(container.getId()); + item.setName(container.getName()); + item.setImage(container.getImage()); + item.setState(container.getState()); + item.setStatus(container.getStatus()); + item.setHealthStatus(container.getHealthStatus()); + item.setCpuPercent(container.getCpuPercent()); + item.setMemoryUsageBytes(container.getMemoryUsageBytes()); + item.setMemoryLimitBytes(container.getMemoryLimitBytes()); + item.setMemoryPercent(container.getMemoryPercent()); + item.setNetworkRxBytes(container.getNetworkRxBytes()); + item.setNetworkTxBytes(container.getNetworkTxBytes()); + item.setUpdatedAt(container.getUpdatedAt()); + result.add(item); + } + return result; + } + } + + @Override + public DockerContainerStatusView getContainerStatus(String containerId) { + synchronized (dockerStatusStore) { + DockerStatusStore.Container container = findContainer(containerId); + if (container == null) { + return null; + } + DockerContainerStatusView view = new DockerContainerStatusView(); + view.setId(container.getId()); + view.setName(container.getName()); + view.setImage(container.getImage()); + view.setImageId(container.getImageId()); + view.setCreatedAt(container.getCreatedAt()); + view.setState(container.getState()); + view.setStatus(container.getStatus()); + view.setHealthStatus(container.getHealthStatus()); + view.setStartedAt(container.getStartedAt()); + view.setFinishedAt(container.getFinishedAt()); + view.setExitCode(container.getExitCode()); + view.setRestartCount(container.getRestartCount()); + view.setOomKilled(container.isOomKilled()); + view.setCpuPercent(container.getCpuPercent()); + view.setMemoryUsageBytes(container.getMemoryUsageBytes()); + view.setMemoryLimitBytes(container.getMemoryLimitBytes()); + view.setMemoryPercent(container.getMemoryPercent()); + view.setNetworkRxBytes(container.getNetworkRxBytes()); + view.setNetworkTxBytes(container.getNetworkTxBytes()); + view.setBlockReadBytes(container.getBlockReadBytes()); + view.setBlockWriteBytes(container.getBlockWriteBytes()); + view.setPids(container.getPids()); + view.setUpdatedAt(container.getUpdatedAt()); + return view; + } + } + + @Override + public DockerContainerHistoryView getContainerHistory(String containerId, String window) { + synchronized (dockerStatusStore) { + DockerStatusStore.Container container = findContainer(containerId); + if (container == null) { + return null; + } + DockerContainerHistoryView view = new DockerContainerHistoryView(); + view.setId(container.getId()); + view.setName(container.getName()); + view.setServerTime(Time.now()); + view.setSampleRateMs(collectRateMs); + + long windowMs = parseWindowMs(window); + long threshold = Time.now() - windowMs; + for (DockerStatusStore.Point point : container.getHistory()) { + if (point.getAt() < threshold) { + continue; + } + if (0 == view.getFrom()) { + view.setFrom(point.getAt()); + } + view.setTo(point.getAt()); + DockerContainerHistoryPointView item = new DockerContainerHistoryPointView(); + item.setAt(point.getAt()); + item.setCpuPercent(point.getCpuPercent()); + item.setMemoryUsageBytes(point.getMemoryUsageBytes()); + item.setMemoryPercent(point.getMemoryPercent()); + item.setNetworkRxBytes(point.getNetworkRxBytes()); + item.setNetworkTxBytes(point.getNetworkTxBytes()); + item.setBlockReadBytes(point.getBlockReadBytes()); + item.setBlockWriteBytes(point.getBlockWriteBytes()); + item.setPids(point.getPids()); + view.getPoints().add(item); + } + return view; + } + } + + private long parseWindowMs(String window) { + if (window == null || window.isBlank()) { + return 60L * collectRateMs; + } + + String normalized = window.trim().toLowerCase(); + char suffix = normalized.charAt(normalized.length() - 1); + long unit = switch (suffix) { + case 's' -> 1000L; + case 'm' -> 60_000L; + case 'h' -> 3_600_000L; + case 'd' -> 86_400_000L; + default -> collectRateMs; + }; + String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1); + try { + return Math.max(collectRateMs, Long.parseLong(valueText) * unit); + } catch (NumberFormatException e) { + return 60L * collectRateMs; + } + } + + /** + * 按容器 ID 或前缀查找容器 + * + * @param containerId 容器 ID + * @return 容器缓存 + */ + private DockerStatusStore.Container findContainer(String containerId) { + DockerStatusStore.Container exact = dockerStatusStore.getContainers().get(containerId); + if (exact != null) { + return exact; + } + for (DockerStatusStore.Container container : dockerStatusStore.getContainers().values()) { + if (container.getId() != null && container.getId().startsWith(containerId)) { + return container; + } + } + return null; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java new file mode 100644 index 0000000..b496556 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/StatusServiceImplement.java @@ -0,0 +1,414 @@ +package com.imyeyu.api.modules.system.service.implement; + +import com.imyeyu.api.modules.common.bean.SettingKey; +import com.imyeyu.api.modules.common.service.SettingService; +import com.imyeyu.api.modules.system.bean.ServerStatus; +import com.imyeyu.api.modules.system.service.StatusService; +import com.imyeyu.api.modules.system.vo.SystemStatusDataView; +import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView; +import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; + +/** + * 系统状态查询服务实现 + * + * @author Codex + * @since 2026-04-06 + */ +@Service +@RequiredArgsConstructor +public class StatusServiceImplement implements StatusService { + + private final ServerStatus serverStatus; + private final SettingService settingService; + + @Override + public SystemStatusSnapshotView getStatus(String metrics) { + long serverTime = Time.now(); + int sampleRateMs = settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE); + EnumSet selectedMetrics = parseMetrics(metrics); + + synchronized (serverStatus) { + SystemStatusSnapshotView view = new SystemStatusSnapshotView(); + view.setServerTime(serverTime); + view.setSampleRateMs(sampleRateMs); + view.setSnapshot(buildSnapshot(serverTime, selectedMetrics)); + return view; + } + } + + @Override + public SystemStatusHistoryView getStatusHistory(String window, String metrics) { + long serverTime = Time.now(); + int sampleRateMs = settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE); + EnumSet selectedMetrics = parseMetrics(metrics); + + synchronized (serverStatus) { + SystemStatusHistoryView history = buildHistory(window, sampleRateMs, selectedMetrics); + history.setServerTime(serverTime); + history.setSampleRateMs(sampleRateMs); + return history; + } + } + + /** + * 构建当前快照 + * + * @param serverTime 服务端时间 + * @param selectedMetrics 指标集合 + * @return 当前快照 + */ + private SystemStatusDataView.Snapshot buildSnapshot(long serverTime, EnumSet selectedMetrics) { + SystemStatusDataView.Snapshot snapshot = new SystemStatusDataView.Snapshot(); + if (selectedMetrics.contains(Metric.OS)) { + SystemStatusDataView.OS os = new SystemStatusDataView.OS(); + os.setName(serverStatus.getOs().getName()); + os.setBootAt(serverStatus.getOs().getBootAt()); + os.setUptimeMs(Math.max(0, serverTime - serverStatus.getOs().getBootAt())); + snapshot.setOs(os); + } + if (selectedMetrics.contains(Metric.CPU)) { + SystemStatusDataView.CPU cpu = new SystemStatusDataView.CPU(); + cpu.setModel(serverStatus.getCpu().getName()); + cpu.setPhysicalCores(serverStatus.getCpu().getCoreCount()); + cpu.setLogicalCores(serverStatus.getCpu().getLogicalCount()); + cpu.setUsagePercent(lastDouble(serverStatus.getCpu().getUsed())); + cpu.setSystemPercent(lastDouble(serverStatus.getCpu().getSystem())); + cpu.setTemperatureCelsius(serverStatus.getCpu().getTemperature()); + snapshot.setCpu(cpu); + } + if (selectedMetrics.contains(Metric.MEMORY)) { + SystemStatusDataView.Memory memory = new SystemStatusDataView.Memory(); + Long usedBytes = lastLong(serverStatus.getMemory().getUsed()); + Long swapUsedBytes = lastLong(serverStatus.getMemory().getSwapUsed()); + memory.setTotalBytes(serverStatus.getMemory().getSize()); + memory.setUsedBytes(usedBytes); + memory.setUsagePercent(toPercent(usedBytes, serverStatus.getMemory().getSize())); + memory.setSwapTotalBytes(serverStatus.getMemory().getSwapSize()); + memory.setSwapUsedBytes(swapUsedBytes); + snapshot.setMemory(memory); + } + if (selectedMetrics.contains(Metric.JVM)) { + SystemStatusDataView.JVM jvm = new SystemStatusDataView.JVM(); + SystemStatusDataView.GC gc = new SystemStatusDataView.GC(); + jvm.setName(serverStatus.getJvm().getName()); + jvm.setVersion(serverStatus.getJvm().getVersion()); + jvm.setBootAt(serverStatus.getJvm().getBootAt()); + jvm.setHeapInitBytes(serverStatus.getJvm().getMemory().getInit()); + jvm.setHeapMaxBytes(serverStatus.getJvm().getMemory().getMax()); + jvm.setHeapUsedBytes(lastLong(serverStatus.getJvm().getMemory().getUsed())); + jvm.setHeapCommittedBytes(lastLong(serverStatus.getJvm().getMemory().getCommitted())); + gc.setCollector(serverStatus.getJvm().getGcName()); + gc.setCycleCount(serverStatus.getJvm().getGc().getSyncCycles()); + gc.setPauseCount(serverStatus.getJvm().getGc().getPauses()); + gc.setLastPauseAt(serverStatus.getJvm().getGc().getLastPauseAt()); + gc.setLastRecoveredBytes(serverStatus.getJvm().getGc().getLastRecoverySize()); + jvm.setGc(gc); + snapshot.setJvm(jvm); + } + if (selectedMetrics.contains(Metric.NETWORK)) { + SystemStatusDataView.Network network = new SystemStatusDataView.Network(); + network.setInterfaceName(serverStatus.getNetwork().getName()); + network.setMac(serverStatus.getNetwork().getMac()); + network.setRxBytesPerSecond(serverStatus.getNetwork().getRecvNow()); + network.setTxBytesPerSecond(serverStatus.getNetwork().getSentNow()); + network.setRxTotalBytes(serverStatus.getNetwork().getRecvTotal()); + network.setTxTotalBytes(serverStatus.getNetwork().getSentTotal()); + network.setRxPacketsTotal(serverStatus.getNetwork().getRecvPacketsTotal()); + network.setTxPacketsTotal(serverStatus.getNetwork().getSentPacketsTotal()); + network.setInErrors(serverStatus.getNetwork().getInErrors()); + network.setOutErrors(serverStatus.getNetwork().getOutErrors()); + network.setInDrops(serverStatus.getNetwork().getInDrops()); + network.setCollisions(serverStatus.getNetwork().getCollisions()); + snapshot.setNetwork(network); + } + if (selectedMetrics.contains(Metric.HARDWARE)) { + SystemStatusDataView.Hardware hardware = new SystemStatusDataView.Hardware(); + SystemStatusDataView.Baseboard baseboard = new SystemStatusDataView.Baseboard(); + SystemStatusDataView.Firmware firmware = new SystemStatusDataView.Firmware(); + hardware.setFanSpeeds(new ArrayList<>(serverStatus.getHardware().getFanSpeeds())); + baseboard.setManufacturer(serverStatus.getHardware().getBaseboard().getManufacturer()); + baseboard.setModel(serverStatus.getHardware().getBaseboard().getModel()); + baseboard.setVersion(serverStatus.getHardware().getBaseboard().getVersion()); + baseboard.setSerialNumber(serverStatus.getHardware().getBaseboard().getSerialNumber()); + firmware.setManufacturer(serverStatus.getHardware().getFirmware().getManufacturer()); + firmware.setName(serverStatus.getHardware().getFirmware().getName()); + firmware.setDescription(serverStatus.getHardware().getFirmware().getDescription()); + firmware.setVersion(serverStatus.getHardware().getFirmware().getVersion()); + firmware.setReleaseDate(serverStatus.getHardware().getFirmware().getReleaseDate()); + hardware.setBaseboard(baseboard); + hardware.setFirmware(firmware); + snapshot.setHardware(hardware); + } + if (selectedMetrics.contains(Metric.STORAGE)) { + List storagePartitions = new ArrayList<>(); + for (ServerStatus.StoragePartition partition : serverStatus.getStoragePartitions()) { + SystemStatusDataView.StoragePartition item = new SystemStatusDataView.StoragePartition(); + item.setDiskName(partition.getDiskName()); + item.setDiskModel(partition.getDiskModel()); + item.setDiskSerial(partition.getDiskSerial()); + item.setPartitionId(partition.getPartitionId()); + item.setPartitionName(partition.getPartitionName()); + item.setPartitionType(partition.getPartitionType()); + item.setUuid(partition.getUuid()); + item.setMountPoint(partition.getMountPoint()); + item.setTotalBytes(partition.getTotalBytes()); + item.setUsedBytes(partition.getUsedBytes()); + item.setUsagePercent(toPercent(partition.getUsedBytes(), partition.getTotalBytes())); + item.setTransferTimeMs(partition.getTransferTimeMs()); + storagePartitions.add(item); + } + snapshot.setStoragePartitions(storagePartitions); + } + return snapshot; + } + + /** + * 构建历史数据 + * + * @param window 历史窗口 + * @param sampleRateMs 采样周期 + * @param selectedMetrics 指标集合 + * @return 历史数据 + */ + private SystemStatusHistoryView buildHistory(String window, int sampleRateMs, EnumSet selectedMetrics) { + SystemStatusHistoryView view = new SystemStatusHistoryView(); + List axis = copyLongs(serverStatus.getUpdateAxis()); + if (axis.isEmpty()) { + return view; + } + + int startIndex = resolveStartIndex(axis, parseWindowMs(window, sampleRateMs)); + view.setFrom(axis.get(startIndex)); + view.setTo(axis.get(axis.size() - 1)); + + List cpuUsed = selectedMetrics.contains(Metric.CPU) ? copyDoubles(serverStatus.getCpu().getUsed()) : List.of(); + List cpuSystem = selectedMetrics.contains(Metric.CPU) ? copyDoubles(serverStatus.getCpu().getSystem()) : List.of(); + List memoryUsed = selectedMetrics.contains(Metric.MEMORY) ? copyLongs(serverStatus.getMemory().getUsed()) : List.of(); + List swapUsed = selectedMetrics.contains(Metric.MEMORY) ? copyLongs(serverStatus.getMemory().getSwapUsed()) : List.of(); + List heapUsed = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getMemory().getUsed()) : List.of(); + List heapCommitted = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getMemory().getCommitted()) : List.of(); + List gcCycleTime = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getGc().getSyncCyclesTime()) : List.of(); + List gcPauseTime = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getGc().getPausesTime()) : List.of(); + List rx = selectedMetrics.contains(Metric.NETWORK) ? toRate(copyLongs(serverStatus.getNetwork().getRecv()), sampleRateMs) : List.of(); + List tx = selectedMetrics.contains(Metric.NETWORK) ? toRate(copyLongs(serverStatus.getNetwork().getSent()), sampleRateMs) : List.of(); + + for (int index = startIndex; index < axis.size(); index++) { + SystemStatusDataView.Point point = new SystemStatusDataView.Point(); + point.setAt(axis.get(index)); + if (selectedMetrics.contains(Metric.CPU)) { + point.setCpuUsagePercent(getAlignedValue(cpuUsed, axis.size(), index)); + point.setCpuSystemPercent(getAlignedValue(cpuSystem, axis.size(), index)); + } + if (selectedMetrics.contains(Metric.MEMORY)) { + point.setMemoryUsedBytes(getAlignedValue(memoryUsed, axis.size(), index)); + point.setSwapUsedBytes(getAlignedValue(swapUsed, axis.size(), index)); + } + if (selectedMetrics.contains(Metric.JVM)) { + point.setHeapUsedBytes(getAlignedValue(heapUsed, axis.size(), index)); + point.setHeapCommittedBytes(getAlignedValue(heapCommitted, axis.size(), index)); + point.setGcCycleTimeMs(getAlignedValue(gcCycleTime, axis.size(), index)); + point.setGcPauseTimeMs(getAlignedValue(gcPauseTime, axis.size(), index)); + } + if (selectedMetrics.contains(Metric.NETWORK)) { + point.setRxBytesPerSecond(getAlignedValue(rx, axis.size(), index)); + point.setTxBytesPerSecond(getAlignedValue(tx, axis.size(), index)); + } + view.getPoints().add(point); + } + return view; + } + + /** + * 解析指标 + * + * @param metrics 指标字符串 + * @return 指标集合 + */ + private EnumSet parseMetrics(String metrics) { + if (metrics == null || metrics.isBlank()) { + return EnumSet.allOf(Metric.class); + } + EnumSet selected = EnumSet.noneOf(Metric.class); + for (String metric : metrics.split(",")) { + switch (metric.trim().toLowerCase(Locale.ROOT)) { + case "os" -> selected.add(Metric.OS); + case "cpu" -> selected.add(Metric.CPU); + case "memory" -> selected.add(Metric.MEMORY); + case "jvm", "gc" -> selected.add(Metric.JVM); + case "network" -> selected.add(Metric.NETWORK); + case "storage", "disk", "disks" -> selected.add(Metric.STORAGE); + case "hardware", "board", "bios", "fan", "fans" -> selected.add(Metric.HARDWARE); + default -> { + } + } + } + return selected.isEmpty() ? EnumSet.allOf(Metric.class) : selected; + } + + /** + * 解析窗口毫秒数 + * + * @param window 窗口字符串 + * @param sampleRateMs 采样周期 + * @return 窗口毫秒数 + */ + private long parseWindowMs(String window, int sampleRateMs) { + if (window == null || window.isBlank()) { + return (long) settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) * sampleRateMs; + } + + String normalized = window.trim().toLowerCase(Locale.ROOT); + char suffix = normalized.charAt(normalized.length() - 1); + long unit = switch (suffix) { + case 's' -> 1000L; + case 'm' -> 60_000L; + case 'h' -> 3_600_000L; + case 'd' -> 86_400_000L; + default -> sampleRateMs; + }; + String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1); + try { + return Math.max(sampleRateMs, Long.parseLong(valueText) * unit); + } catch (NumberFormatException e) { + return (long) settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) * sampleRateMs; + } + } + + /** + * 解析历史起点 + * + * @param axis 时间轴 + * @param windowMs 窗口毫秒数 + * @return 起点下标 + */ + private int resolveStartIndex(List axis, long windowMs) { + long threshold = axis.get(axis.size() - 1) - windowMs; + for (int index = 0; index < axis.size(); index++) { + if (threshold <= axis.get(index)) { + return index; + } + } + return axis.size() - 1; + } + + /** + * 将周期累计值转换为每秒速率 + * + * @param source 原始列表 + * @param sampleRateMs 采样周期 + * @return 速率列表 + */ + private List toRate(List source, int sampleRateMs) { + List result = new ArrayList<>(source.size()); + for (Long value : source) { + result.add(value == null ? null : value * 1000 / sampleRateMs); + } + return result; + } + + /** + * 获取与时间轴尾部对齐的值 + * + * @param values 数据列表 + * @param axisSize 时间轴长度 + * @param axisIndex 时间轴下标 + * @param 数据类型 + * @return 对齐后的值 + */ + private T getAlignedValue(List values, int axisSize, int axisIndex) { + int valueIndex = axisIndex - (axisSize - values.size()); + if (valueIndex < 0 || values.size() <= valueIndex) { + return null; + } + return values.get(valueIndex); + } + + /** + * 复制长整型列表 + * + * @param source 原始队列 + * @return 列表 + */ + private List copyLongs(Deque source) { + List result = new ArrayList<>(source.size()); + for (Number number : source) { + result.add(number == null ? null : number.longValue()); + } + return result; + } + + /** + * 复制浮点列表 + * + * @param source 原始队列 + * @return 列表 + */ + private List copyDoubles(Deque source) { + List result = new ArrayList<>(source.size()); + for (Number number : source) { + result.add(number == null ? null : number.doubleValue()); + } + return result; + } + + /** + * 获取最后一个长整型值 + * + * @param source 队列 + * @return 值 + */ + private Long lastLong(Deque source) { + Number number = source.peekLast(); + return number == null ? null : number.longValue(); + } + + /** + * 获取最后一个浮点值 + * + * @param source 队列 + * @return 值 + */ + private Double lastDouble(Deque source) { + Number number = source.peekLast(); + return number == null ? null : number.doubleValue(); + } + + /** + * 计算百分比 + * + * @param used 已用值 + * @param total 总值 + * @return 百分比 + */ + private Double toPercent(Long used, long total) { + if (used == null || total <= 0) { + return null; + } + return used * 100D / total; + } + + /** + * 状态指标 + * + * @author Codex + * @since 2026-04-06 + */ + private enum Metric { + OS, + CPU, + MEMORY, + JVM, + NETWORK, + HARDWARE, + STORAGE + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/UpsServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/UpsServiceImplement.java new file mode 100644 index 0000000..d7f2c87 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/UpsServiceImplement.java @@ -0,0 +1,151 @@ +package com.imyeyu.api.modules.system.service.implement; + +import com.imyeyu.api.modules.system.bean.UpsStatusStore; +import com.imyeyu.api.modules.system.service.UpsService; +import com.imyeyu.api.modules.system.task.UpsStatusTask; +import com.imyeyu.api.modules.system.vo.ups.UpsHistoryPointView; +import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView; +import com.imyeyu.api.modules.system.vo.ups.UpsStatusView; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * UPS 查询服务实现 + * + * @author Codex + * @since 2026-04-07 + */ +@Service +@RequiredArgsConstructor +public class UpsServiceImplement implements UpsService { + + private final UpsStatusTask upsStatusTask; + private final UpsStatusStore upsStatusStore; + + @Value("${ups.collect-rate-ms:60000}") + private long collectRateMs; + + @Override + public UpsStatusView getStatus() { + ensureCurrentSnapshot(); + synchronized (upsStatusStore) { + UpsStatusStore.Snapshot snapshot = upsStatusStore.getCurrent(); + if (snapshot == null) { + return null; + } + UpsStatusView view = new UpsStatusView(); + view.setServerTime(System.currentTimeMillis()); + view.setUpsTime(snapshot.getUpsTime()); + view.setHostName(snapshot.getHostName()); + view.setCustomer(snapshot.getCustomer()); + view.setVersion(snapshot.getVersion()); + view.setDeviceId(snapshot.getDeviceId()); + view.setUpsType(snapshot.getUpsType()); + view.setMorphological(snapshot.getMorphological()); + view.setIoPhase(snapshot.getIoPhase()); + view.setWorkMode(snapshot.getWorkMode()); + view.setInputVoltage(snapshot.getInputVoltage()); + view.setInputFrequency(snapshot.getInputFrequency()); + view.setOutputVoltage(snapshot.getOutputVoltage()); + view.setOutputFrequency(snapshot.getOutputFrequency()); + view.setOutputLoadPercent(snapshot.getOutputLoadPercent()); + view.setBatteryVoltage(snapshot.getBatteryVoltage()); + view.setBatteryCapacity(snapshot.getBatteryCapacity()); + view.setBatteryRemainTime(snapshot.getBatteryRemainTime()); + view.setTemperature(snapshot.getTemperature()); + view.setBypassActive(snapshot.isBypassActive()); + view.setShutdownActive(snapshot.isShutdownActive()); + view.setOutputOn(snapshot.isOutputOn()); + view.setCharging(snapshot.isCharging()); + view.setFaultType(snapshot.getFaultType()); + view.setFaultKind(snapshot.getFaultKind()); + view.setWarnings(snapshot.getWarnings()); + return view; + } + } + + @Override + public UpsHistoryView getHistory(String window) { + ensureCurrentSnapshot(); + UpsHistoryView view = new UpsHistoryView(); + view.setServerTime(System.currentTimeMillis()); + view.setSampleRateMs(collectRateMs); + + long windowMs = parseWindowMs(window); + long threshold = view.getServerTime() - windowMs; + + synchronized (upsStatusStore) { + for (UpsStatusStore.Point point : upsStatusStore.getHistory()) { + if (point.getAt() < threshold) { + continue; + } + if (0 == view.getFrom()) { + view.setFrom(point.getAt()); + } + view.setTo(point.getAt()); + UpsHistoryPointView item = new UpsHistoryPointView(); + item.setAt(point.getAt()); + item.setUpsTime(point.getUpsTime()); + item.setWorkMode(point.getWorkMode()); + item.setInputVoltage(point.getInputVoltage()); + item.setInputFrequency(point.getInputFrequency()); + item.setOutputVoltage(point.getOutputVoltage()); + item.setOutputFrequency(point.getOutputFrequency()); + item.setOutputLoadPercent(point.getOutputLoadPercent()); + item.setBatteryVoltage(point.getBatteryVoltage()); + item.setBatteryCapacity(point.getBatteryCapacity()); + item.setBatteryRemainTime(point.getBatteryRemainTime()); + item.setTemperature(point.getTemperature()); + item.setBypassActive(point.isBypassActive()); + item.setShutdownActive(point.isShutdownActive()); + item.setOutputOn(point.isOutputOn()); + view.getPoints().add(item); + } + } + return view; + } + + /** + * 保证当前快照存在 + */ + private void ensureCurrentSnapshot() { + synchronized (upsStatusStore) { + if (upsStatusStore.getCurrent() != null) { + return; + } + } + upsStatusTask.collectOnce(); + } + + /** + * 解析历史窗口 + * + * @param window 历史窗口 + * @return 窗口毫秒数 + */ + private long parseWindowMs(String window) { + long defaultWindowMs = 24L * 60 * 60 * 1000; + if (window == null || window.isBlank()) { + return defaultWindowMs; + } + + String normalized = window.trim().toLowerCase(); + char suffix = normalized.charAt(normalized.length() - 1); + long unit = switch (suffix) { + case 's' -> 1000L; + case 'm' -> 60_000L; + case 'h' -> 3_600_000L; + case 'd' -> 86_400_000L; + default -> collectRateMs; + }; + String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1); + try { + long parsed = Long.parseLong(valueText) * unit; + long maxWindowMs = UpsStatusTask.MAX_HISTORY_MS; + return Math.max(collectRateMs, Math.min(parsed, maxWindowMs)); + } catch (NumberFormatException e) { + return defaultWindowMs; + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/DockerStatusTask.java b/src/main/java/com/imyeyu/api/modules/system/task/DockerStatusTask.java new file mode 100644 index 0000000..7b35fc7 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/DockerStatusTask.java @@ -0,0 +1,272 @@ +package com.imyeyu.api.modules.system.task; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.imyeyu.api.modules.system.bean.DockerStatusStore; +import com.imyeyu.api.modules.system.util.DockerEngineClient; +import com.imyeyu.utils.Time; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.PeriodicTrigger; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Docker 状态采集任务 + * + * @author Codex + * @since 2026-04-06 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DockerStatusTask implements SchedulingConfigurer { + + private final DockerEngineClient dockerEngineClient; + private final DockerStatusStore dockerStatusStore; + + @Value("${docker.engine.collect-enabled:true}") + private boolean collectEnabled; + + @Value("${docker.engine.collect-rate-ms:10000}") + private long collectRateMs; + + @Value("${docker.engine.history-limit:120}") + private int historyLimit; + + @Override + public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) { + if (!collectEnabled) { + return; + } + PeriodicTrigger trigger = new PeriodicTrigger(collectRateMs, TimeUnit.MILLISECONDS); + trigger.setInitialDelay(0); + taskRegistrar.addTriggerTask(this::collect, trigger); + } + + private void collect() { + try { + JsonArray containers = dockerEngineClient.getJson("/containers/json", DockerEngineClient.query("all", "true")).getAsJsonArray(); + long now = Time.now(); + synchronized (dockerStatusStore) { + Set activeIds = new HashSet<>(); + for (JsonElement item : containers) { + try { + JsonObject summary = item.getAsJsonObject(); + String containerId = getAsString(summary, "Id"); + activeIds.add(containerId); + DockerStatusStore.Container container = dockerStatusStore.getContainers().computeIfAbsent(containerId, key -> new DockerStatusStore.Container()); + updateContainerSummary(container, summary); + updateContainerInspect(containerId, container); + updateContainerStats(containerId, container, now); + } catch (Exception e) { + log.error("collect docker container item error", e); + } + } + dockerStatusStore.getContainers().entrySet().removeIf(item -> !activeIds.contains(item.getKey())); + } + } catch (Exception e) { + log.error("collect docker container status error", e); + } + } + + private void updateContainerSummary(DockerStatusStore.Container container, JsonObject summary) { + container.setId(getAsString(summary, "Id")); + container.setName(trimContainerName(readFirstArrayText(summary, "Names"))); + container.setImage(getAsString(summary, "Image")); + container.setImageId(getAsString(summary, "ImageID")); + container.setCreatedAt(getAsLong(summary, "Created") * 1000); + container.setState(getAsString(summary, "State")); + container.setStatus(getAsString(summary, "Status")); + } + + private void updateContainerInspect(String containerId, DockerStatusStore.Container container) { + JsonObject inspect = dockerEngineClient.getJson("/containers/%s/json".formatted(containerId), Map.of()).getAsJsonObject(); + JsonObject state = getAsObject(inspect, "State"); + container.setStartedAt(getAsString(state, "StartedAt")); + container.setFinishedAt(getAsString(state, "FinishedAt")); + container.setExitCode(getAsInteger(state, "ExitCode")); + container.setRestartCount(getAsInteger(inspect, "RestartCount", 0)); + container.setOomKilled(getAsBoolean(state, "OOMKilled")); + JsonObject health = getAsObject(state, "Health"); + container.setHealthStatus(health == null ? null : getAsString(health, "Status")); + } + + private void updateContainerStats(String containerId, DockerStatusStore.Container container, long now) { + JsonObject stats = dockerEngineClient.getJson("/containers/%s/stats".formatted(containerId), DockerEngineClient.query("stream", "false")).getAsJsonObject(); + Double cpuPercent = calculateCpuPercent(stats); + Long memoryUsageBytes = getNestedLong(stats, "memory_stats", "usage"); + Long memoryLimitBytes = getNestedLong(stats, "memory_stats", "limit"); + Double memoryPercent = null; + if (memoryUsageBytes != null && memoryLimitBytes != null && 0 < memoryLimitBytes) { + memoryPercent = memoryUsageBytes * 100D / memoryLimitBytes; + } + Long networkRxBytes = 0L; + Long networkTxBytes = 0L; + JsonObject networks = getAsObject(stats, "networks"); + if (networks != null) { + for (Map.Entry item : networks.entrySet()) { + JsonObject network = item.getValue().getAsJsonObject(); + networkRxBytes += getAsLong(network, "rx_bytes", 0L); + networkTxBytes += getAsLong(network, "tx_bytes", 0L); + } + } + Long blockReadBytes = 0L; + Long blockWriteBytes = 0L; + JsonObject blkioStats = getAsObject(stats, "blkio_stats"); + JsonArray ioServiceBytes = blkioStats == null ? null : blkioStats.getAsJsonArray("io_service_bytes_recursive"); + if (ioServiceBytes != null) { + for (JsonElement item : ioServiceBytes) { + JsonObject io = item.getAsJsonObject(); + String op = getAsString(io, "op"); + long value = getAsLong(io, "value", 0L); + if ("Read".equalsIgnoreCase(op)) { + blockReadBytes += value; + } else if ("Write".equalsIgnoreCase(op)) { + blockWriteBytes += value; + } + } + } + Integer pids = getNestedInteger(stats, "pids_stats", "current"); + + container.setCpuPercent(cpuPercent); + container.setMemoryUsageBytes(memoryUsageBytes); + container.setMemoryLimitBytes(memoryLimitBytes); + container.setMemoryPercent(memoryPercent); + container.setNetworkRxBytes(networkRxBytes); + container.setNetworkTxBytes(networkTxBytes); + container.setBlockReadBytes(blockReadBytes); + container.setBlockWriteBytes(blockWriteBytes); + container.setPids(pids); + container.setUpdatedAt(now); + + DockerStatusStore.Point point = new DockerStatusStore.Point(); + point.setAt(now); + point.setCpuPercent(cpuPercent); + point.setMemoryUsageBytes(memoryUsageBytes); + point.setMemoryPercent(memoryPercent); + point.setNetworkRxBytes(networkRxBytes); + point.setNetworkTxBytes(networkTxBytes); + point.setBlockReadBytes(blockReadBytes); + point.setBlockWriteBytes(blockWriteBytes); + point.setPids(pids); + container.getHistory().addLast(point); + while (historyLimit < container.getHistory().size()) { + container.getHistory().pollFirst(); + } + } + + private Double calculateCpuPercent(JsonObject stats) { + Long cpuTotal = getNestedLong(stats, "cpu_stats", "cpu_usage", "total_usage"); + Long preCpuTotal = getNestedLong(stats, "precpu_stats", "cpu_usage", "total_usage"); + Long systemTotal = getNestedLong(stats, "cpu_stats", "system_cpu_usage"); + Long preSystemTotal = getNestedLong(stats, "precpu_stats", "system_cpu_usage"); + Integer onlineCpus = getNestedInteger(stats, "cpu_stats", "online_cpus"); + if (onlineCpus == null || onlineCpus <= 0) { + JsonArray perCpuUsage = getNestedArray(stats, "cpu_stats", "cpu_usage", "percpu_usage"); + onlineCpus = perCpuUsage == null ? 1 : perCpuUsage.size(); + } + if (cpuTotal == null || preCpuTotal == null || systemTotal == null || preSystemTotal == null) { + return null; + } + long cpuDelta = cpuTotal - preCpuTotal; + long systemDelta = systemTotal - preSystemTotal; + if (cpuDelta <= 0 || systemDelta <= 0) { + return 0D; + } + return cpuDelta * 100D * onlineCpus / systemDelta; + } + + private JsonObject getAsObject(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return null; + } + return source.getAsJsonObject(key); + } + + private JsonArray getNestedArray(JsonObject source, String... keys) { + JsonElement current = source; + for (String key : keys) { + if (current == null || !current.isJsonObject() || !current.getAsJsonObject().has(key)) { + return null; + } + current = current.getAsJsonObject().get(key); + } + return current != null && current.isJsonArray() ? current.getAsJsonArray() : null; + } + + private Long getNestedLong(JsonObject source, String... keys) { + JsonElement current = source; + for (String key : keys) { + if (current == null || !current.isJsonObject() || !current.getAsJsonObject().has(key)) { + return null; + } + current = current.getAsJsonObject().get(key); + } + return current == null || current.isJsonNull() ? null : current.getAsLong(); + } + + private Integer getNestedInteger(JsonObject source, String... keys) { + Long value = getNestedLong(source, keys); + return value == null ? null : value.intValue(); + } + + private String readFirstArrayText(JsonObject source, String key) { + if (source == null || !source.has(key) || !source.get(key).isJsonArray() || source.getAsJsonArray(key).isEmpty()) { + return null; + } + return source.getAsJsonArray(key).get(0).getAsString(); + } + + private String getAsString(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return null; + } + return source.get(key).getAsString(); + } + + private long getAsLong(JsonObject source, String key) { + return getAsLong(source, key, 0L); + } + + private long getAsLong(JsonObject source, String key, long defaultValue) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return defaultValue; + } + return source.get(key).getAsLong(); + } + + private Integer getAsInteger(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return null; + } + return source.get(key).getAsInt(); + } + + private int getAsInteger(JsonObject source, String key, int defaultValue) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return defaultValue; + } + return source.get(key).getAsInt(); + } + + private boolean getAsBoolean(JsonObject source, String key) { + return source != null && source.has(key) && !source.get(key).isJsonNull() && source.get(key).getAsBoolean(); + } + + private String trimContainerName(String name) { + if (name == null) { + return null; + } + return name.startsWith("/") ? name.substring(1) : name; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/ServerStatusTask.java b/src/main/java/com/imyeyu/api/modules/system/task/ServerStatusTask.java index 8eb7fd0..940ef61 100644 --- a/src/main/java/com/imyeyu/api/modules/system/task/ServerStatusTask.java +++ b/src/main/java/com/imyeyu/api/modules/system/task/ServerStatusTask.java @@ -1,235 +1,72 @@ package com.imyeyu.api.modules.system.task; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import com.imyeyu.utils.OS; -import com.imyeyu.utils.Time; -import com.imyeyu.java.TimiJava; import com.imyeyu.api.modules.common.bean.SettingKey; import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.system.bean.ServerStatus; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import com.imyeyu.api.modules.system.task.status.StatusCollector; +import com.imyeyu.utils.Time; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; import oshi.SystemInfo; -import oshi.hardware.CentralProcessor; -import oshi.hardware.GlobalMemory; import oshi.hardware.HardwareAbstractionLayer; -import oshi.hardware.NetworkIF; -import oshi.software.os.OSFileStore; -import oshi.software.os.OperatingSystem; -import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.util.Deque; import java.util.List; /** - * 服务器状态收集任务 + * 服务端状态采集任务 * * @author 夜雨 - * @version 2022-01-31 15:18 + * @since 2022-01-31 15:18 */ @Slf4j @Service @RequiredArgsConstructor -public class ServerStatusTask implements SchedulingConfigurer, TimiJava { +public class ServerStatusTask implements SchedulingConfigurer { private final ServerStatus status; private final SettingService settingService; - - private GlobalMemory globalMemory; // 内存 - private MemoryMXBean jvmMemory; // JVM 内存 - private OperatingSystem os; // 操作系统 - private CentralProcessor processor; // 中央处理器 - private HardwareAbstractionLayer hardware; // 硬件 - - private long[] lastCPUTicks; // CPU 上一时刻状态 - private long lastLinuxAPIMemoryUsed; // 上一周期 JVM 内存大小 + private final List statusCollectors; @Override public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) { - lastLinuxAPIMemoryUsed = -1; + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); - // 系统信息 - SystemInfo system = new SystemInfo(); - - // 硬件信息 - hardware = system.getHardware(); - - // 操作系统 - os = system.getOperatingSystem(); - - // ---------- 静态数据 ---------- - - // 系统 - status.getOs().setName(OS.NAME); - status.getOs().setBootAt(os.getSystemBootTime() * 1000); - - // JVM - jvmMemory = ManagementFactory.getMemoryMXBean(); - status.getJvm().setBootAt(Time.now()); - status.getJvm().setName(System.getProperty("java.vm.name")); - status.getJvm().setVersion(System.getProperty("java.version")); - - // CPU - processor = hardware.getProcessor(); - status.getCpu().setName(processor.getProcessorIdentifier().getName().trim()); - status.getCpu().setCoreCount(processor.getPhysicalProcessorCount()); - status.getCpu().setLogicalCount(processor.getLogicalProcessorCount()); - - // 内存 - globalMemory = hardware.getMemory(); - status.getMemory().setSize(globalMemory.getTotal()); - status.getMemory().setSwapSize(globalMemory.getVirtualMemory().getSwapTotal()); - - // 网卡 - List networkIFs = hardware.getNetworkIFs(); - { - NetworkIF networkIF; - boolean isFound = false; - for (int i = 0; i < networkIFs.size(); i++) { - networkIF = networkIFs.get(i); - if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) { - status.getNetwork().setMac(networkIF.getMacaddr()); - - status.getNetwork().setRecvTotal(networkIF.getBytesRecv()); - status.getNetwork().setSentTotal(networkIF.getBytesSent()); - - isFound = true; - break; - } - } - if (!isFound) { - log.error("not found setting networkIF MAC: %s" + settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC)); - for (int i = 0; i < networkIFs.size(); i++) { - log.info("Network Interface: {} -> {}", networkIFs.get(i).getMacaddr(), networkIFs.get(i).getDisplayName()); - } + StatusCollectContext context = new StatusCollectContext( + status, + hardware.getMemory(), + ManagementFactory.getMemoryMXBean(), + settingService, + hardware.getComputerSystem(), + systemInfo.getOperatingSystem(), + hardware.getProcessor(), + hardware, + Time.now() + ); + synchronized (status) { + for (StatusCollector collector : statusCollectors) { + collector.initialize(context); } } taskRegistrar.addTriggerTask(() -> { - long now = Time.now(); - - // ---------- JVM 内存 ---------- - long nowLinuxAPIMemoryUsed = jvmMemory.getHeapMemoryUsage().getUsed(); - long linuxAPIMemoryDiff = Math.abs(nowLinuxAPIMemoryUsed - lastLinuxAPIMemoryUsed); - lastLinuxAPIMemoryUsed = nowLinuxAPIMemoryUsed; - status.getJvm().getMemory().setInit(jvmMemory.getHeapMemoryUsage().getInit()); - status.getJvm().getMemory().setMax(jvmMemory.getHeapMemoryUsage().getMax()); - putDeque(status.getJvm().getMemory().getUsed(), nowLinuxAPIMemoryUsed); - putDeque(status.getJvm().getMemory().getCommitted(), jvmMemory.getHeapMemoryUsage().getCommitted()); - - // ---------- JVM GC ---------- - long gcSyncCycles = 0; - long gcSyncCyclesTime = 0; - long gcPauses = 0; - long gcPausesTime = 0; - for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { - gcSyncCycles += gc.getCollectionCount(); - gcSyncCyclesTime += gc.getCollectionTime(); - switch (gc.getName()) { - case "ZGC Cycles" -> { - gcSyncCycles += gc.getCollectionCount(); - gcSyncCyclesTime += gc.getCollectionTime(); - } - case "ZGC Pauses" -> { - gcPauses += gc.getCollectionCount(); - gcPausesTime += gc.getCollectionTime(); - if (status.getJvm().getZgc().getPauses() < gcPauses) { - // 发生 GC 回收 - status.getJvm().getZgc().setLastPauseAt(now); - status.getJvm().getZgc().setLastRecoverySize(linuxAPIMemoryDiff); - } - } + synchronized (status) { + context.setCollectAt(Time.now()); + for (StatusCollector collector : statusCollectors) { + collector.collect(context); + } + status.getUpdateAxis().addLast(context.getCollectAt()); + if (settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < status.getUpdateAxis().size()) { + status.getUpdateAxis().pollFirst(); } } - putDeque(status.getJvm().getZgc().getSyncCyclesTime(), gcSyncCyclesTime - status.getJvm().getZgc().getSyncCyclesTimeTotal()); - putDeque(status.getJvm().getZgc().getPausesTime(), gcPausesTime - status.getJvm().getZgc().getPausesTimeTotal()); - status.getJvm().getZgc().setSyncCycles(gcSyncCycles); - status.getJvm().getZgc().setSyncCyclesTimeTotal(gcSyncCyclesTime); - status.getJvm().getZgc().setPauses(gcPauses); - status.getJvm().getZgc().setPausesTimeTotal(gcPausesTime); - - // ---------- CPU ---------- - if (lastCPUTicks != null) { - long[] ticks = processor.getSystemCpuLoadTicks(); - - long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCPUTicks[CentralProcessor.TickType.USER.getIndex()]; - long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.NICE.getIndex()]; - long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; - long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IDLE.getIndex()]; - long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; - long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IRQ.getIndex()]; - long softIRQ = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; - long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCPUTicks[CentralProcessor.TickType.STEAL.getIndex()]; - long total = user + nice + sys + idle + ioWait + irq + softIRQ + steal; - - putDeque(status.getCpu().getSystem(), 100D * sys / total); - putDeque(status.getCpu().getUsed(), 100 - 100D * idle / total); - } - lastCPUTicks = processor.getSystemCpuLoadTicks(); - status.getCpu().setTemperature(hardware.getSensors().getCpuTemperature()); - - // ---------- 内存 ---------- - putDeque(status.getMemory().getUsed(), globalMemory.getTotal() - globalMemory.getAvailable()); - putDeque(status.getMemory().getSwapUsed(), globalMemory.getVirtualMemory().getSwapUsed()); - - // ---------- 网络 ---------- - networkIFs.clear(); - networkIFs.addAll(hardware.getNetworkIFs()); - NetworkIF networkIF; - for (int i = 0; i < networkIFs.size(); i++) { - networkIF = networkIFs.get(i); - if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) { - long recv = networkIF.getBytesRecv() - status.getNetwork().getRecvTotal(); - long sent = networkIF.getBytesSent() - status.getNetwork().getSentTotal(); - status.getNetwork().setRecvNow(recv / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)); - status.getNetwork().setSentNow(sent / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)); - status.getNetwork().setRecvTotal(networkIF.getBytesRecv()); - status.getNetwork().setSentTotal(networkIF.getBytesSent()); - - putDeque(status.getNetwork().getRecv(), recv); - putDeque(status.getNetwork().getSent(), sent); - break; - } - } - - // ---------- 磁盘分区 ---------- - ServerStatus.Partition partition; - status.getPartitions().clear(); - // 分区从文件系统获取,而非物理分区 - List fileStores = os.getFileSystem().getFileStores(); - for (OSFileStore fileStore : fileStores) { - partition = new ServerStatus.Partition(); - partition.setUuid(fileStore.getUUID()); - partition.setPath(fileStore.getMount()); - partition.setType(fileStore.getType()); - partition.setUsed(fileStore.getTotalSpace() - fileStore.getUsableSpace()); - partition.setTotal(fileStore.getTotalSpace()); - - status.getPartitions().add(partition); - } - - // ---------- 更新时轴 ---------- - putDeque(status.getUpdateAxis(), Time.now()); - }, tc -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)).nextExecution(tc)); - } - - /** - * 有所限制地添加队列数据,达到配置 {@link SettingKey#SYSTEM_STATUS_LIMIT} 个时移除最旧的 - * - * @param deque 队列 - * @param t 数据 - * @param 数据类型 - */ - private void putDeque(Deque deque, T t) { - deque.addLast(t); - if (settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < deque.size()) { - deque.pollFirst(); - } + }, triggerContext -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)).nextExecution(triggerContext)); } } diff --git a/src/main/java/com/imyeyu/api/modules/system/task/UpsStatusTask.java b/src/main/java/com/imyeyu/api/modules/system/task/UpsStatusTask.java new file mode 100644 index 0000000..1064bc9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/UpsStatusTask.java @@ -0,0 +1,89 @@ +package com.imyeyu.api.modules.system.task; + +import com.imyeyu.api.modules.system.bean.UpsStatusStore; +import com.imyeyu.api.modules.system.util.UpsStatusClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.PeriodicTrigger; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * UPS 状态采集任务 + * + * @author Codex + * @since 2026-04-07 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UpsStatusTask implements SchedulingConfigurer { + + public static final long MAX_HISTORY_MS = 3L * 24 * 60 * 60 * 1000; + + private final UpsStatusClient upsStatusClient; + private final UpsStatusStore upsStatusStore; + + @Value("${ups.collect-enabled:true}") + private boolean collectEnabled; + + @Value("${ups.collect-rate-ms:60000}") + private long collectRateMs; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + if (!collectEnabled) { + return; + } + PeriodicTrigger trigger = new PeriodicTrigger(collectRateMs, TimeUnit.MILLISECONDS); + trigger.setInitialDelay(0); + taskRegistrar.addTriggerTask(this::collect, trigger); + } + + /** + * 立即采集一次 + */ + public void collectOnce() { + collect(); + } + + private void collect() { + try { + UpsStatusStore.Snapshot snapshot = upsStatusClient.fetchSnapshot(); + long now = System.currentTimeMillis(); + snapshot.setUpdatedAt(now); + + UpsStatusStore.Point point = new UpsStatusStore.Point(); + point.setAt(now); + point.setUpsTime(snapshot.getUpsTime()); + point.setWorkMode(snapshot.getWorkMode()); + point.setInputVoltage(snapshot.getInputVoltage()); + point.setInputFrequency(snapshot.getInputFrequency()); + point.setOutputVoltage(snapshot.getOutputVoltage()); + point.setOutputFrequency(snapshot.getOutputFrequency()); + point.setOutputLoadPercent(snapshot.getOutputLoadPercent()); + point.setBatteryVoltage(snapshot.getBatteryVoltage()); + point.setBatteryCapacity(snapshot.getBatteryCapacity()); + point.setBatteryRemainTime(snapshot.getBatteryRemainTime()); + point.setTemperature(snapshot.getTemperature()); + point.setBypassActive(snapshot.isBypassActive()); + point.setShutdownActive(snapshot.isShutdownActive()); + point.setOutputOn(snapshot.isOutputOn()); + + synchronized (upsStatusStore) { + upsStatusStore.setCurrent(snapshot); + upsStatusStore.getHistory().addLast(point); + long threshold = now - MAX_HISTORY_MS; + while (!upsStatusStore.getHistory().isEmpty() && upsStatusStore.getHistory().peekFirst().getAt() < threshold) { + upsStatusStore.getHistory().pollFirst(); + } + } + } catch (Exception e) { + log.error("collect ups status error", e); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java new file mode 100644 index 0000000..3aa2791 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/AbstractDequeStatusCollector.java @@ -0,0 +1,29 @@ +package com.imyeyu.api.modules.system.task.status; + +import com.imyeyu.api.modules.common.bean.SettingKey; + +import java.util.Deque; + +/** + * 带有限长队列工具的采集器基类 + * + * @author Codex + * @since 2026-04-06 + */ +public abstract class AbstractDequeStatusCollector implements StatusCollector { + + /** + * 有限长度追加队列数据 + * + * @param context 采集上下文 + * @param deque 队列 + * @param value 值 + * @param 类型 + */ + protected void putDeque(StatusCollectContext context, Deque deque, T value) { + deque.addLast(value); + if (context.getSettingService().getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < deque.size()) { + deque.pollFirst(); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollectContext.java b/src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollectContext.java new file mode 100644 index 0000000..26588b6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollectContext.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.system.task.status; + +import com.imyeyu.api.modules.common.service.SettingService; +import com.imyeyu.api.modules.system.bean.ServerStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import oshi.hardware.CentralProcessor; +import oshi.hardware.ComputerSystem; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.OperatingSystem; + +import java.lang.management.MemoryMXBean; + +/** + * 状态采集上下文 + * + * @author Codex + * @since 2026-04-06 + */ +@Getter +@AllArgsConstructor +public class StatusCollectContext { + + private final ServerStatus status; + private final GlobalMemory globalMemory; + private final MemoryMXBean jvmMemory; + private final SettingService settingService; + private final ComputerSystem computerSystem; + private final OperatingSystem operatingSystem; + private final CentralProcessor processor; + private final HardwareAbstractionLayer hardware; + + @Setter + private long collectAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollector.java new file mode 100644 index 0000000..f164713 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/StatusCollector.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.system.task.status; + +/** + * 状态采集器 + * + * @author Codex + * @since 2026-04-06 + */ +public interface StatusCollector { + + /** + * 初始化采集器 + * + * @param context 采集上下文 + */ + default void initialize(StatusCollectContext context) { + } + + /** + * 采集状态 + * + * @param context 采集上下文 + */ + void collect(StatusCollectContext context); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/CpuStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/CpuStatusCollector.java new file mode 100644 index 0000000..6e4ac04 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/CpuStatusCollector.java @@ -0,0 +1,50 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import oshi.hardware.CentralProcessor; + +/** + * CPU 状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:18 + */ +@Component +@Order(20) +public class CpuStatusCollector extends AbstractDequeStatusCollector { + + private long[] lastCpuTicks; + + @Override + public void initialize(StatusCollectContext context) { + context.getStatus().getCpu().setName(context.getProcessor().getProcessorIdentifier().getName().trim()); + context.getStatus().getCpu().setCoreCount(context.getProcessor().getPhysicalProcessorCount()); + context.getStatus().getCpu().setLogicalCount(context.getProcessor().getLogicalProcessorCount()); + lastCpuTicks = context.getProcessor().getSystemCpuLoadTicks(); + } + + @Override + public void collect(StatusCollectContext context) { + long[] ticks = context.getProcessor().getSystemCpuLoadTicks(); + if (lastCpuTicks != null) { + long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCpuTicks[CentralProcessor.TickType.USER.getIndex()]; + long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCpuTicks[CentralProcessor.TickType.NICE.getIndex()]; + long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCpuTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; + long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IDLE.getIndex()]; + long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; + long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IRQ.getIndex()]; + long softIrq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCpuTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; + long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCpuTicks[CentralProcessor.TickType.STEAL.getIndex()]; + long total = user + nice + sys + idle + ioWait + irq + softIrq + steal; + if (0 < total) { + putDeque(context, context.getStatus().getCpu().getSystem(), 100D * sys / total); + putDeque(context, context.getStatus().getCpu().getUsed(), 100 - 100D * idle / total); + } + } + lastCpuTicks = ticks; + context.getStatus().getCpu().setTemperature(context.getHardware().getSensors().getCpuTemperature()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/HardwareStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/HardwareStatusCollector.java new file mode 100644 index 0000000..50be5ab --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/HardwareStatusCollector.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.system.bean.ServerStatus; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import com.imyeyu.api.modules.system.task.status.StatusCollector; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; + +/** + * 硬件状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:18 + */ +@Component +@Order(35) +public class HardwareStatusCollector implements StatusCollector { + + @Override + public void initialize(StatusCollectContext context) { + ServerStatus.Baseboard baseboard = context.getStatus().getHardware().getBaseboard(); + baseboard.setManufacturer(context.getComputerSystem().getBaseboard().getManufacturer()); + baseboard.setModel(context.getComputerSystem().getBaseboard().getModel()); + baseboard.setVersion(context.getComputerSystem().getBaseboard().getVersion()); + baseboard.setSerialNumber(context.getComputerSystem().getBaseboard().getSerialNumber()); + + ServerStatus.Firmware firmware = context.getStatus().getHardware().getFirmware(); + firmware.setManufacturer(context.getComputerSystem().getFirmware().getManufacturer()); + firmware.setName(context.getComputerSystem().getFirmware().getName()); + firmware.setDescription(context.getComputerSystem().getFirmware().getDescription()); + firmware.setVersion(context.getComputerSystem().getFirmware().getVersion()); + firmware.setReleaseDate(context.getComputerSystem().getFirmware().getReleaseDate()); + } + + @Override + public void collect(StatusCollectContext context) { + int[] fanSpeeds = context.getHardware().getSensors().getFanSpeeds(); + ArrayList values = new ArrayList<>(fanSpeeds.length); + for (int fanSpeed : fanSpeeds) { + values.add(fanSpeed); + } + context.getStatus().getHardware().setFanSpeeds(values); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/JvmStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/JvmStatusCollector.java new file mode 100644 index 0000000..a7c4c67 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/JvmStatusCollector.java @@ -0,0 +1,100 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import com.imyeyu.utils.Time; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.List; + +/** + * JVM 状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:17 + */ +@Component +@Order(40) +public class JvmStatusCollector extends AbstractDequeStatusCollector { + + private long lastHeapUsed = -1; + + @Override + public void initialize(StatusCollectContext context) { + context.getStatus().getJvm().setBootAt(Time.now()); + context.getStatus().getJvm().setName(System.getProperty("java.vm.name")); + context.getStatus().getJvm().setVersion(System.getProperty("java.version")); + context.getStatus().getJvm().setGcName(resolveGcName()); + } + + @Override + public void collect(StatusCollectContext context) { + long heapUsed = context.getJvmMemory().getHeapMemoryUsage().getUsed(); + context.getStatus().getJvm().getMemory().setInit(context.getJvmMemory().getHeapMemoryUsage().getInit()); + context.getStatus().getJvm().getMemory().setMax(context.getJvmMemory().getHeapMemoryUsage().getMax()); + putDeque(context, context.getStatus().getJvm().getMemory().getUsed(), heapUsed); + putDeque(context, context.getStatus().getJvm().getMemory().getCommitted(), context.getJvmMemory().getHeapMemoryUsage().getCommitted()); + + long recoverySize = 0; + if (0 <= lastHeapUsed) { + recoverySize = Math.abs(heapUsed - lastHeapUsed); + } + lastHeapUsed = heapUsed; + + long gcSyncCycles = 0; + long gcSyncCyclesTime = 0; + long gcPauses = 0; + long gcPausesTime = 0; + for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { + switch (gc.getName()) { + case "ZGC Cycles" -> { + gcSyncCycles += gc.getCollectionCount(); + gcSyncCyclesTime += gc.getCollectionTime(); + } + case "ZGC Pauses" -> { + gcPauses += gc.getCollectionCount(); + gcPausesTime += gc.getCollectionTime(); + } + default -> { + } + } + } + + if (context.getStatus().getJvm().getGc().getPauses() < gcPauses) { + context.getStatus().getJvm().getGc().setLastPauseAt(context.getCollectAt()); + context.getStatus().getJvm().getGc().setLastRecoverySize(recoverySize); + } + putDeque(context, context.getStatus().getJvm().getGc().getSyncCyclesTime(), gcSyncCyclesTime - context.getStatus().getJvm().getGc().getSyncCyclesTimeTotal()); + putDeque(context, context.getStatus().getJvm().getGc().getPausesTime(), gcPausesTime - context.getStatus().getJvm().getGc().getPausesTimeTotal()); + context.getStatus().getJvm().getGc().setSyncCycles(gcSyncCycles); + context.getStatus().getJvm().getGc().setSyncCyclesTimeTotal(gcSyncCyclesTime); + context.getStatus().getJvm().getGc().setPauses(gcPauses); + context.getStatus().getJvm().getGc().setPausesTimeTotal(gcPausesTime); + } + + /** + * 解析当前 JVM 的主要 GC 名称 + * + * @return GC 名称 + */ + private String resolveGcName() { + List collectors = ManagementFactory.getGarbageCollectorMXBeans(); + if (collectors.isEmpty()) { + return null; + } + if (1 == collectors.size()) { + return collectors.get(0).getName(); + } + StringBuilder gcName = new StringBuilder(); + for (GarbageCollectorMXBean collector : collectors) { + if (!gcName.isEmpty()) { + gcName.append(", "); + } + gcName.append(collector.getName()); + } + return gcName.toString(); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/MemoryStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/MemoryStatusCollector.java new file mode 100644 index 0000000..bc01c5c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/MemoryStatusCollector.java @@ -0,0 +1,29 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 系统内存状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:17 + */ +@Component +@Order(30) +public class MemoryStatusCollector extends AbstractDequeStatusCollector { + + @Override + public void initialize(StatusCollectContext context) { + context.getStatus().getMemory().setSize(context.getGlobalMemory().getTotal()); + context.getStatus().getMemory().setSwapSize(context.getGlobalMemory().getVirtualMemory().getSwapTotal()); + } + + @Override + public void collect(StatusCollectContext context) { + putDeque(context, context.getStatus().getMemory().getUsed(), context.getGlobalMemory().getTotal() - context.getGlobalMemory().getAvailable()); + putDeque(context, context.getStatus().getMemory().getSwapUsed(), context.getGlobalMemory().getVirtualMemory().getSwapUsed()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java new file mode 100644 index 0000000..5c3d390 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/NetworkStatusCollector.java @@ -0,0 +1,80 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.common.bean.SettingKey; +import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import lombok.extern.slf4j.Slf4j; +import org.bytedeco.librealsense.context; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import oshi.hardware.NetworkIF; + +import java.util.List; + +/** + * 网络状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:15 + */ +@Slf4j +@Component +@Order(50) +public class NetworkStatusCollector extends AbstractDequeStatusCollector { + + @Override + public void initialize(StatusCollectContext context) { + List networkIFs = context.getHardware().getNetworkIFs(); + String targetMac = context.getSettingService().getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC); + for (NetworkIF networkIF : networkIFs) { + if (networkIF.getMacaddr().equals(targetMac)) { + networkIF.updateAttributes(); + updateNetworkStatus(context, networkIF); + return; + } + } + + log.error("not found setting networkIF MAC: {}", targetMac); + for (NetworkIF networkIF : networkIFs) { + log.info("Network Interface: {} -> {}", networkIF.getMacaddr(), networkIF.getDisplayName()); + } + } + + @Override + public void collect(StatusCollectContext context) { + String targetMac = context.getSettingService().getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC); + int sampleRateMs = context.getSettingService().getAsInt(SettingKey.SYSTEM_STATUS_RATE); + for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) { + if (networkIF.getMacaddr().equals(targetMac)) { + networkIF.updateAttributes(); + long recv = networkIF.getBytesRecv() - context.getStatus().getNetwork().getRecvTotal(); + long sent = networkIF.getBytesSent() - context.getStatus().getNetwork().getSentTotal(); + context.getStatus().getNetwork().setRecvNow(recv * 1000 / sampleRateMs); + context.getStatus().getNetwork().setSentNow(sent * 1000 / sampleRateMs); + updateNetworkStatus(context, networkIF); + putDeque(context, context.getStatus().getNetwork().getRecv(), recv); + putDeque(context, context.getStatus().getNetwork().getSent(), sent); + return; + } + } + } + + /** + * 更新网络状态缓存 + * + * @param context 采集上下文 + * @param networkIF 网卡 + */ + private void updateNetworkStatus(StatusCollectContext context, NetworkIF networkIF) { + context.getStatus().getNetwork().setName(networkIF.getDisplayName()); + context.getStatus().getNetwork().setMac(networkIF.getMacaddr()); + context.getStatus().getNetwork().setRecvTotal(networkIF.getBytesRecv()); + context.getStatus().getNetwork().setSentTotal(networkIF.getBytesSent()); + context.getStatus().getNetwork().setRecvPacketsTotal(networkIF.getPacketsRecv()); + context.getStatus().getNetwork().setSentPacketsTotal(networkIF.getPacketsSent()); + context.getStatus().getNetwork().setInErrors(networkIF.getInErrors()); + context.getStatus().getNetwork().setOutErrors(networkIF.getOutErrors()); + context.getStatus().getNetwork().setInDrops(networkIF.getInDrops()); + context.getStatus().getNetwork().setCollisions(networkIF.getCollisions()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/OSStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/OSStatusCollector.java new file mode 100644 index 0000000..184c27d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/OSStatusCollector.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import com.imyeyu.api.modules.system.task.status.StatusCollector; +import com.imyeyu.utils.OS; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 操作系统状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:15 + */ +@Component +@Order(10) +public class OSStatusCollector implements StatusCollector { + + @Override + public void initialize(StatusCollectContext context) { + context.getStatus().getOs().setName(OS.NAME); + context.getStatus().getOs().setBootAt(context.getOperatingSystem().getSystemBootTime() * 1000); + } + + @Override + public void collect(StatusCollectContext context) { + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/task/status/collector/StorageStatusCollector.java b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/StorageStatusCollector.java new file mode 100644 index 0000000..edbd1cb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/task/status/collector/StorageStatusCollector.java @@ -0,0 +1,90 @@ +package com.imyeyu.api.modules.system.task.status.collector; + +import com.imyeyu.api.modules.system.bean.ServerStatus; +import com.imyeyu.api.modules.system.task.status.StatusCollectContext; +import com.imyeyu.api.modules.system.task.status.StatusCollector; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import oshi.hardware.HWDiskStore; +import oshi.hardware.HWPartition; +import oshi.software.os.OSFileStore; + +import java.util.HashMap; +import java.util.Map; + +/** + * 存储状态采集器 + * + * @author 夜雨 + * @since 2026-04-07 11:14 + */ +@Component +@Order(60) +public class StorageStatusCollector implements StatusCollector { + + @Override + public void collect(StatusCollectContext context) { + Map fileStoreMap = createFileStoreMap(context); + context.getStatus().getStoragePartitions().clear(); + for (HWDiskStore diskStore : context.getHardware().getDiskStores()) { + diskStore.updateAttributes(); + for (HWPartition partition : diskStore.getPartitions()) { + ServerStatus.StoragePartition item = new ServerStatus.StoragePartition(); + item.setDiskName(diskStore.getName()); + item.setDiskModel(diskStore.getModel()); + item.setDiskSerial(diskStore.getSerial()); + item.setPartitionId(partition.getIdentification()); + item.setPartitionName(partition.getName()); + item.setPartitionType(partition.getType()); + item.setUuid(partition.getUuid()); + item.setMountPoint(partition.getMountPoint()); + item.setTotalBytes(partition.getSize()); + item.setTransferTimeMs(diskStore.getTransferTime()); + + OSFileStore fileStore = matchFileStore(partition, fileStoreMap); + if (fileStore != null) { + fileStore.updateAttributes(); + item.setUsedBytes(fileStore.getTotalSpace() - fileStore.getUsableSpace()); + } + context.getStatus().getStoragePartitions().add(item); + } + } + } + + /** + * 创建文件系统映射 + * + * @param context 采集上下文 + * @return 映射表 + */ + private Map createFileStoreMap(StatusCollectContext context) { + Map result = new HashMap<>(); + for (OSFileStore fileStore : context.getOperatingSystem().getFileSystem().getFileStores()) { + result.put("mount:" + fileStore.getMount(), fileStore); + result.put("volume:" + fileStore.getVolume(), fileStore); + result.put("name:" + fileStore.getName(), fileStore); + } + return result; + } + + /** + * 匹配文件系统 + * + * @param partition 物理分区 + * @param fileStoreMap 文件系统映射 + * @return 文件系统 + */ + private OSFileStore matchFileStore(HWPartition partition, Map fileStoreMap) { + if (partition.getMountPoint() != null && !partition.getMountPoint().isBlank()) { + OSFileStore byMount = fileStoreMap.get("mount:" + partition.getMountPoint()); + if (byMount != null) { + return byMount; + } + } + OSFileStore byName = fileStoreMap.get("name:" + partition.getName()); + if (byName != null) { + return byName; + } + return fileStoreMap.get("volume:" + partition.getIdentification()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/util/DockerEngineClient.java b/src/main/java/com/imyeyu/api/modules/system/util/DockerEngineClient.java new file mode 100644 index 0000000..8e02292 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/util/DockerEngineClient.java @@ -0,0 +1,274 @@ +package com.imyeyu.api.modules.system.util; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.UnixDomainSocketAddress; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Docker Engine API 客户端 + * + * @author Codex + * @since 2026-04-06 + */ +@Slf4j +@Component +public class DockerEngineClient { + + private final String host; + private final String apiVersion; + private final Duration timeout; + private final HttpClient httpClient = HttpClient.newBuilder().build(); + + public DockerEngineClient( + @Value("${docker.engine.host:unix:///var/run/docker.sock}") String host, + @Value("${docker.engine.api-version:v1.41}") String apiVersion, + @Value("${docker.engine.timeout-ms:5000}") long timeoutMs + ) { + this.host = host; + this.apiVersion = apiVersion; + this.timeout = Duration.ofMillis(timeoutMs); + } + + /** + * 获取 JSON 响应 + * + * @param path 接口路径 + * @param queryParams 查询参数 + * @return JSON 数据 + */ + public JsonElement getJson(String path, Map queryParams) { + String requestPath = buildRequestPath(path, queryParams); + try { + String body = host.startsWith("unix://") + ? executeUnixGet(requestPath) + : executeHttpGet(requestPath); + return JsonParser.parseString(body); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("docker engine request interrupted: {}", requestPath, e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO docker engine request interrupted"); + } catch (IOException e) { + log.error("docker engine request error: {}", requestPath, e); + throw new TimiException(TimiCode.ERROR).msgKey("TODO docker engine request error"); + } + } + + /** + * 构建请求路径 + * + * @param path 接口路径 + * @param queryParams 查询参数 + * @return 请求路径 + */ + private String buildRequestPath(String path, Map queryParams) { + StringBuilder builder = new StringBuilder(); + builder.append("/"); + builder.append(apiVersion); + if (!path.startsWith("/")) { + builder.append("/"); + } + builder.append(path); + if (queryParams != null && !queryParams.isEmpty()) { + builder.append("?"); + boolean first = true; + for (Map.Entry item : queryParams.entrySet()) { + if (!first) { + builder.append("&"); + } + first = false; + builder.append(item.getKey()).append("=").append(item.getValue()); + } + } + return builder.toString(); + } + + /** + * 通过 HTTP/TCP 执行 GET + * + * @param requestPath 请求路径 + * @return 响应体 + * @throws IOException IO 异常 + * @throws InterruptedException 中断异常 + */ + private String executeHttpGet(String requestPath) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(host + requestPath)) + .timeout(timeout) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (400 <= response.statusCode()) { + throw new IOException("docker engine http error: " + response.statusCode()); + } + return response.body(); + } + + /** + * 通过 Unix Socket 执行 GET + * + * @param requestPath 请求路径 + * @return 响应体 + * @throws IOException IO 异常 + */ + private String executeUnixGet(String requestPath) throws IOException { + String socketPath = host.substring("unix://".length()); + UnixDomainSocketAddress address = UnixDomainSocketAddress.of(Path.of(socketPath)); + try (SocketChannel channel = SocketChannel.open(address)) { + channel.configureBlocking(true); + String requestText = """ + GET %s HTTP/1.1\r + Host: docker\r + Accept: application/json\r + Connection: close\r + \r + """.formatted(requestPath); + channel.write(StandardCharsets.UTF_8.encode(requestText)); + byte[] responseBytes = readAll(channel); + return parseHttpBody(responseBytes); + } + } + + /** + * 读取全部响应字节 + * + * @param channel 通道 + * @return 字节数组 + * @throws IOException IO 异常 + */ + private byte[] readAll(SocketChannel channel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(8192); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + while (true) { + int readLength = channel.read(buffer); + if (readLength < 0) { + break; + } + if (0 == readLength) { + continue; + } + buffer.flip(); + output.write(buffer.array(), 0, buffer.remaining()); + buffer.clear(); + } + return output.toByteArray(); + } + + /** + * 解析 HTTP 响应体 + * + * @param responseBytes 响应字节 + * @return 响应体 + * @throws IOException IO 异常 + */ + private String parseHttpBody(byte[] responseBytes) throws IOException { + int splitIndex = indexOf(responseBytes, new byte[] {'\r', '\n', '\r', '\n'}); + if (splitIndex < 0) { + throw new IOException("invalid docker engine response"); + } + String headerText = new String(responseBytes, 0, splitIndex, StandardCharsets.UTF_8); + byte[] bodyBytes = new byte[responseBytes.length - splitIndex - 4]; + System.arraycopy(responseBytes, splitIndex + 4, bodyBytes, 0, bodyBytes.length); + if (headerText.contains("Transfer-Encoding: chunked")) { + bodyBytes = decodeChunkedBody(bodyBytes); + } + if (!headerText.startsWith("HTTP/1.1 200") && !headerText.startsWith("HTTP/1.1 204")) { + throw new IOException("docker engine http error: " + headerText.lines().findFirst().orElse(headerText)); + } + return new String(bodyBytes, StandardCharsets.UTF_8); + } + + /** + * 解码 Chunked 响应体 + * + * @param bodyBytes 原始响应体 + * @return 解码后的响应体 + * @throws IOException IO 异常 + */ + private byte[] decodeChunkedBody(byte[] bodyBytes) throws IOException { + int offset = 0; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + while (offset < bodyBytes.length) { + int lineEnd = indexOf(bodyBytes, offset, new byte[] {'\r', '\n'}); + if (lineEnd < 0) { + break; + } + String sizeText = new String(bodyBytes, offset, lineEnd - offset, StandardCharsets.UTF_8).trim(); + int chunkSize = Integer.parseInt(sizeText, 16); + offset = lineEnd + 2; + if (0 == chunkSize) { + break; + } + output.write(bodyBytes, offset, chunkSize); + offset += chunkSize + 2; + } + return output.toByteArray(); + } + + /** + * 查找字节序列 + * + * @param source 原数组 + * @param pattern 目标数组 + * @return 下标 + */ + private int indexOf(byte[] source, byte[] pattern) { + return indexOf(source, 0, pattern); + } + + /** + * 从指定位置查找字节序列 + * + * @param source 原数组 + * @param start 起始位置 + * @param pattern 目标数组 + * @return 下标 + */ + private int indexOf(byte[] source, int start, byte[] pattern) { + for (int i = start; i <= source.length - pattern.length; i++) { + boolean matched = true; + for (int j = 0; j < pattern.length; j++) { + if (source[i + j] != pattern[j]) { + matched = false; + break; + } + } + if (matched) { + return i; + } + } + return -1; + } + + /** + * 创建查询参数映射 + * + * @param entries 键值对 + * @return 映射 + */ + public static Map query(String... entries) { + LinkedHashMap result = new LinkedHashMap<>(); + for (int i = 0; i + 1 < entries.length; i += 2) { + result.put(entries[i], entries[i + 1]); + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/util/SystemAPIInterceptor.java b/src/main/java/com/imyeyu/api/modules/system/util/SystemAPIInterceptor.java index 05c417e..8e52fa6 100644 --- a/src/main/java/com/imyeyu/api/modules/system/util/SystemAPIInterceptor.java +++ b/src/main/java/com/imyeyu/api/modules/system/util/SystemAPIInterceptor.java @@ -29,9 +29,9 @@ public class SystemAPIInterceptor implements HandlerInterceptor { private final SettingService settingService; public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) { - String key = TimiSpring.getHeader("Key"); + String key = TimiSpring.getHeader("Token"); if (TimiJava.isEmpty(key)) { - key = req.getParameter("key"); + key = req.getParameter("token"); } String dbKey = settingService.getAsString(SettingKey.SYSTEM_API_KEY); String dbSuperKey = settingService.getAsString(SettingKey.SYSTEM_API_SUPER_KEY); diff --git a/src/main/java/com/imyeyu/api/modules/system/util/UpsStatusClient.java b/src/main/java/com/imyeyu/api/modules/system/util/UpsStatusClient.java new file mode 100644 index 0000000..12baac0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/util/UpsStatusClient.java @@ -0,0 +1,265 @@ +package com.imyeyu.api.modules.system.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.imyeyu.api.modules.system.bean.UpsStatusStore; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +/** + * UPS 状态客户端 + * + * @author Codex + * @since 2026-04-07 + */ +@Slf4j +@Component +public class UpsStatusClient { + + private final String statusUrl; + private final Duration timeout; + private final HttpClient httpClient = HttpClient.newBuilder().build(); + + public UpsStatusClient( + @Value("${ups.status-url:}") String statusUrl, + @Value("${ups.timeout-ms:5000}") long timeoutMs + ) { + this.statusUrl = statusUrl; + this.timeout = Duration.ofMillis(timeoutMs); + } + + /** + * 获取 UPS 状态 JSON + * + * @return JSON 数据 + */ + public JsonElement getStatusJson() { + if (statusUrl == null || statusUrl.isBlank()) { + throw new TimiException(TimiCode.ARG_MISS).msgKey("缺少配置:ups.status-url"); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(statusUrl)) + .timeout(timeout) + .GET() + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (400 <= response.statusCode()) { + throw new IOException("ups http error: " + response.statusCode()); + } + return JsonParser.parseString(response.body()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("request ups status interrupted: {}", statusUrl, e); + throw new TimiException(TimiCode.ERROR).msgKey("UPS 状态请求被中断"); + } catch (IOException e) { + log.error("request ups status error: {}", statusUrl, e); + throw new TimiException(TimiCode.ERROR).msgKey("UPS 状态请求失败"); + } + } + + /** + * 拉取并解析 UPS 当前快照 + * + * @return UPS 当前快照 + */ + public UpsStatusStore.Snapshot fetchSnapshot() { + JsonObject root = getStatusJson().getAsJsonObject(); + JsonObject workInfo = getAsObject(root, "workInfo"); + + UpsStatusStore.Snapshot snapshot = new UpsStatusStore.Snapshot(); + snapshot.setUpsTime(readUpsTime(workInfo)); + snapshot.setHostName(readMeaningfulText(root, "hostName")); + snapshot.setCustomer(readMeaningfulText(root, "customer")); + snapshot.setVersion(readMeaningfulText(root, "version")); + snapshot.setDeviceId(readMeaningfulText(workInfo, "deviceId")); + snapshot.setUpsType(readMeaningfulText(workInfo, "upsType")); + snapshot.setMorphological(readMeaningfulText(workInfo, "morphological")); + snapshot.setIoPhase(readMeaningfulText(workInfo, "ioPhase")); + snapshot.setWorkMode(readMeaningfulText(workInfo, "workMode")); + snapshot.setInputVoltage(readMeaningfulDouble(workInfo, "inputVoltage")); + snapshot.setInputFrequency(readMeaningfulDouble(workInfo, "inputFrequency")); + snapshot.setOutputVoltage(readMeaningfulDouble(workInfo, "outputVoltage")); + snapshot.setOutputFrequency(readMeaningfulDouble(workInfo, "outputFrequency")); + snapshot.setOutputLoadPercent(readMeaningfulInteger(workInfo, "outputLoadPercent")); + snapshot.setBatteryVoltage(readMeaningfulDouble(workInfo, "batteryVoltage")); + snapshot.setBatteryCapacity(readMeaningfulInteger(workInfo, "batteryCapacity")); + snapshot.setBatteryRemainTime(readMeaningfulInteger(workInfo, "batteryRemainTime")); + snapshot.setTemperature(readMeaningfulDouble(workInfo, "temperatureView")); + snapshot.setBypassActive(readAsBoolean(workInfo, "bypassActive")); + snapshot.setShutdownActive(readAsBoolean(workInfo, "shutdownActive")); + snapshot.setOutputOn(readAsBoolean(workInfo, "outputON")); + snapshot.setCharging(readAsBoolean(workInfo, "chargeON")); + snapshot.setFaultType(readMeaningfulText(workInfo, "faultType")); + snapshot.setFaultKind(readMeaningfulText(workInfo, "faultKind")); + snapshot.setWarnings(readWarnings(workInfo)); + return snapshot; + } + + /** + * 读取 UPS 时间戳 + * + * @param workInfo 工作信息 + * @return 时间戳 + */ + private Long readUpsTime(JsonObject workInfo) { + JsonObject currentTime = getAsObject(workInfo, "currentTime"); + if (currentTime == null || !currentTime.has("time") || currentTime.get("time").isJsonNull()) { + return null; + } + return currentTime.get("time").getAsLong(); + } + + /** + * 读取告警列表 + * + * @param workInfo 工作信息 + * @return 告警列表 + */ + private java.util.List readWarnings(JsonObject workInfo) { + java.util.List warnings = new java.util.ArrayList<>(); + if (workInfo == null || !workInfo.has("warnings") || !workInfo.get("warnings").isJsonArray()) { + return warnings; + } + JsonArray array = workInfo.getAsJsonArray("warnings"); + for (JsonElement item : array) { + if (!item.isJsonPrimitive()) { + continue; + } + String warning = normalizeText(item.getAsString()); + if (warning != null) { + warnings.add(warning); + } + } + return warnings; + } + + /** + * 读取对象字段 + * + * @param source 源对象 + * @param key 键 + * @return 子对象 + */ + private JsonObject getAsObject(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull() || !source.get(key).isJsonObject()) { + return null; + } + return source.getAsJsonObject(key); + } + + /** + * 读取有效文本 + * + * @param source 源对象 + * @param key 键 + * @return 文本 + */ + private String readMeaningfulText(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return null; + } + return normalizeText(source.get(key).getAsString()); + } + + /** + * 读取有效整数 + * + * @param source 源对象 + * @param key 键 + * @return 整数 + */ + private Integer readMeaningfulInteger(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return null; + } + JsonElement element = source.get(key); + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) { + return element.getAsInt(); + } + String text = normalizeText(element.getAsString()); + if (text == null) { + return null; + } + try { + return Integer.parseInt(text); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * 读取有效小数 + * + * @param source 源对象 + * @param key 键 + * @return 小数 + */ + private Double readMeaningfulDouble(JsonObject source, String key) { + if (source == null || !source.has(key) || source.get(key).isJsonNull()) { + return null; + } + JsonElement element = source.get(key); + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) { + return element.getAsDouble(); + } + String text = normalizeText(element.getAsString()); + if (text == null) { + return null; + } + try { + return Double.parseDouble(text); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * 读取布尔值 + * + * @param source 源对象 + * @param key 键 + * @return 布尔值 + */ + private boolean readAsBoolean(JsonObject source, String key) { + return source != null && source.has(key) && !source.get(key).isJsonNull() && source.get(key).getAsBoolean(); + } + + /** + * 规范化文本 + * + * @param text 原始文本 + * @return 规范化后的文本 + */ + private String normalizeText(String text) { + if (text == null) { + return null; + } + + String normalized = text.trim(); + if (normalized.isEmpty()) { + return null; + } + if ("----".equals(normalized)) { + return null; + } + if (normalized.startsWith("----:")) { + return null; + } + return normalized; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusDataView.java b/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusDataView.java new file mode 100644 index 0000000..a13a587 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusDataView.java @@ -0,0 +1,381 @@ +package com.imyeyu.api.modules.system.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 系统状态数据视图 + * + * @author Codex + * @since 2026-04-06 + */ +public class SystemStatusDataView { + + /** + * 当前快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Snapshot { + + /** 操作系统 */ + private OS os; + + /** CPU */ + private CPU cpu; + + /** 系统内存 */ + private Memory memory; + + /** JVM */ + private JVM jvm; + + /** 网络 */ + private Network network; + + /** 硬件 */ + private Hardware hardware; + + /** 存储分区 */ + private List storagePartitions = new ArrayList<>(); + } + + /** + * 历史点 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Point { + + /** 采样时间 */ + private long at; + + /** CPU 总占用 */ + private Double cpuUsagePercent; + + /** CPU 系统占用 */ + private Double cpuSystemPercent; + + /** 系统已用内存 */ + private Long memoryUsedBytes; + + /** 已用交换分区 */ + private Long swapUsedBytes; + + /** JVM 已用堆内存 */ + private Long heapUsedBytes; + + /** JVM 已提交堆内存 */ + private Long heapCommittedBytes; + + /** GC 周期耗时 */ + private Long gcCycleTimeMs; + + /** GC 暂停耗时 */ + private Long gcPauseTimeMs; + + /** 接收速率 */ + private Long rxBytesPerSecond; + + /** 发送速率 */ + private Long txBytesPerSecond; + } + + /** + * 操作系统快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class OS { + + /** 系统名称 */ + private String name; + + /** 启动时间 */ + private long bootAt; + + /** 运行时长 */ + private long uptimeMs; + } + + /** + * CPU 快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class CPU { + + /** 型号 */ + private String model; + + /** 物理核心数 */ + private int physicalCores; + + /** 逻辑核心数 */ + private int logicalCores; + + /** 总占用 */ + private Double usagePercent; + + /** 系统占用 */ + private Double systemPercent; + + /** 温度 */ + private double temperatureCelsius; + } + + /** + * 内存快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Memory { + + /** 总内存 */ + private long totalBytes; + + /** 已用内存 */ + private Long usedBytes; + + /** 使用率 */ + private Double usagePercent; + + /** 交换分区总量 */ + private long swapTotalBytes; + + /** 已用交换分区 */ + private Long swapUsedBytes; + } + + /** + * JVM 快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class JVM { + + /** 名称 */ + private String name; + + /** 版本 */ + private String version; + + /** 启动时间 */ + private long bootAt; + + /** 初始堆大小 */ + private long heapInitBytes; + + /** 最大堆大小 */ + private long heapMaxBytes; + + /** 已用堆大小 */ + private Long heapUsedBytes; + + /** 已提交堆大小 */ + private Long heapCommittedBytes; + + /** GC */ + private GC gc; + } + + /** + * GC 快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class GC { + + /** 收集器名称 */ + private String collector; + + /** 周期次数 */ + private long cycleCount; + + /** 暂停次数 */ + private long pauseCount; + + /** 上次暂停时间 */ + private long lastPauseAt; + + /** 上次回收大小 */ + private long lastRecoveredBytes; + } + + /** + * 网络快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Network { + + /** 网卡名称 */ + private String interfaceName; + + /** MAC 地址 */ + private String mac; + + /** 接收速率 */ + private long rxBytesPerSecond; + + /** 发送速率 */ + private long txBytesPerSecond; + + /** 累计接收 */ + private long rxTotalBytes; + + /** 累计发送 */ + private long txTotalBytes; + + /** 接收包总数 */ + private long rxPacketsTotal; + + /** 发送包总数 */ + private long txPacketsTotal; + + /** 输入错误包总数 */ + private long inErrors; + + /** 输出错误包总数 */ + private long outErrors; + + /** 输入丢弃包总数 */ + private long inDrops; + + /** 碰撞总数 */ + private long collisions; + } + + /** + * 硬件快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Hardware { + + /** 风扇转速 */ + private List fanSpeeds = new ArrayList<>(); + + /** 主板信息 */ + private Baseboard baseboard; + + /** BIOS 信息 */ + private Firmware firmware; + } + + /** + * 主板快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Baseboard { + + /** 厂商 */ + private String manufacturer; + + /** 型号 */ + private String model; + + /** 版本 */ + private String version; + + /** 序列号 */ + private String serialNumber; + } + + /** + * BIOS 快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class Firmware { + + /** 厂商 */ + private String manufacturer; + + /** 名称 */ + private String name; + + /** 描述 */ + private String description; + + /** 版本 */ + private String version; + + /** 发布时间 */ + private String releaseDate; + } + + /** + * 存储分区快照 + * + * @author Codex + * @since 2026-04-06 + */ + @Data + public static class StoragePartition { + + /** 物理磁盘名称 */ + private String diskName; + + /** 物理磁盘型号 */ + private String diskModel; + + /** 物理磁盘序列号 */ + private String diskSerial; + + /** 分区标识 */ + private String partitionId; + + /** 分区名称 */ + private String partitionName; + + /** 分区类型 */ + private String partitionType; + + /** 分区 UUID */ + private String uuid; + + /** 挂载点 */ + private String mountPoint; + + /** 分区总空间 */ + private long totalBytes; + + /** 已用空间 */ + private Long usedBytes; + + /** 使用率 */ + private Double usagePercent; + + /** 传输耗时 */ + private long transferTimeMs; + + /** 健康状态 */ + private String healthStatus; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusHistoryView.java b/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusHistoryView.java new file mode 100644 index 0000000..fa530c9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusHistoryView.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.system.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 系统状态历史视图 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +public class SystemStatusHistoryView { + + /** 服务端当前时间 */ + private long serverTime; + + /** 采样周期 */ + private int sampleRateMs; + + /** 起始时间 */ + private long from; + + /** 结束时间 */ + private long to; + + /** 历史点 */ + private List points = new ArrayList<>(); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusSnapshotView.java b/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusSnapshotView.java new file mode 100644 index 0000000..2b2ece1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/SystemStatusSnapshotView.java @@ -0,0 +1,22 @@ +package com.imyeyu.api.modules.system.vo; + +import lombok.Data; + +/** + * 系统状态快照视图 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +public class SystemStatusSnapshotView { + + /** 服务端当前时间 */ + private long serverTime; + + /** 采样周期 */ + private int sampleRateMs; + + /** 当前快照 */ + private SystemStatusDataView.Snapshot snapshot = new SystemStatusDataView.Snapshot(); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryPointView.java b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryPointView.java new file mode 100644 index 0000000..62eb55b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryPointView.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.system.vo.docker; + +import lombok.Data; + +/** + * Docker 容器历史点视图 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +public class DockerContainerHistoryPointView { + + /** 时间 */ + private long at; + + /** CPU 百分比 */ + private Double cpuPercent; + + /** 内存使用量 */ + private Long memoryUsageBytes; + + /** 内存百分比 */ + private Double memoryPercent; + + /** 网络接收字节 */ + private Long networkRxBytes; + + /** 网络发送字节 */ + private Long networkTxBytes; + + /** 块设备读取字节 */ + private Long blockReadBytes; + + /** 块设备写入字节 */ + private Long blockWriteBytes; + + /** 进程数 */ + private Integer pids; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryView.java b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryView.java new file mode 100644 index 0000000..031c8e3 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerHistoryView.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.system.vo.docker; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * Docker 容器历史视图 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +public class DockerContainerHistoryView { + + /** 容器 ID */ + private String id; + + /** 容器名称 */ + private String name; + + /** 服务端当前时间 */ + private long serverTime; + + /** 采样周期 */ + private long sampleRateMs; + + /** 起始时间 */ + private long from; + + /** 结束时间 */ + private long to; + + /** 历史点 */ + private List points = new ArrayList<>(); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerStatusView.java b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerStatusView.java new file mode 100644 index 0000000..02df065 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerStatusView.java @@ -0,0 +1,82 @@ +package com.imyeyu.api.modules.system.vo.docker; + +import lombok.Data; + +/** + * Docker 容器状态视图 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +public class DockerContainerStatusView { + + /** 容器 ID */ + private String id; + + /** 容器名称 */ + private String name; + + /** 镜像 */ + private String image; + + /** 镜像 ID */ + private String imageId; + + /** 创建时间 */ + private long createdAt; + + /** 运行状态 */ + private String state; + + /** 状态描述 */ + private String status; + + /** 健康状态 */ + private String healthStatus; + + /** 启动时间 */ + private String startedAt; + + /** 结束时间 */ + private String finishedAt; + + /** 退出码 */ + private Integer exitCode; + + /** 重启次数 */ + private int restartCount; + + /** OOM 标记 */ + private boolean oomKilled; + + /** CPU 百分比 */ + private Double cpuPercent; + + /** 内存使用量 */ + private Long memoryUsageBytes; + + /** 内存限制 */ + private Long memoryLimitBytes; + + /** 内存百分比 */ + private Double memoryPercent; + + /** 网络接收字节 */ + private Long networkRxBytes; + + /** 网络发送字节 */ + private Long networkTxBytes; + + /** 块设备读取字节 */ + private Long blockReadBytes; + + /** 块设备写入字节 */ + private Long blockWriteBytes; + + /** 进程数 */ + private Integer pids; + + /** 更新时间 */ + private long updatedAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerSummaryView.java b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerSummaryView.java new file mode 100644 index 0000000..1cfb298 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/docker/DockerContainerSummaryView.java @@ -0,0 +1,52 @@ +package com.imyeyu.api.modules.system.vo.docker; + +import lombok.Data; + +/** + * Docker 容器摘要视图 + * + * @author Codex + * @since 2026-04-06 + */ +@Data +public class DockerContainerSummaryView { + + /** 容器 ID */ + private String id; + + /** 容器名称 */ + private String name; + + /** 镜像 */ + private String image; + + /** 运行状态 */ + private String state; + + /** 状态描述 */ + private String status; + + /** 健康状态 */ + private String healthStatus; + + /** CPU 百分比 */ + private Double cpuPercent; + + /** 内存使用量 */ + private Long memoryUsageBytes; + + /** 内存限制 */ + private Long memoryLimitBytes; + + /** 内存百分比 */ + private Double memoryPercent; + + /** 网络接收字节 */ + private Long networkRxBytes; + + /** 网络发送字节 */ + private Long networkTxBytes; + + /** 更新时间 */ + private long updatedAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryPointView.java b/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryPointView.java new file mode 100644 index 0000000..46808df --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryPointView.java @@ -0,0 +1,58 @@ +package com.imyeyu.api.modules.system.vo.ups; + +import lombok.Data; + +/** + * UPS 历史点视图 + * + * @author Codex + * @since 2026-04-07 + */ +@Data +public class UpsHistoryPointView { + + /** 采样时间 */ + private long at; + + /** UPS 数据时间 */ + private Long upsTime; + + /** 工作模式 */ + private String workMode; + + /** 输入电压 */ + private Double inputVoltage; + + /** 输入频率 */ + private Double inputFrequency; + + /** 输出电压 */ + private Double outputVoltage; + + /** 输出频率 */ + private Double outputFrequency; + + /** 输出负载百分比 */ + private Integer outputLoadPercent; + + /** 电池电压 */ + private Double batteryVoltage; + + /** 电池容量百分比 */ + private Integer batteryCapacity; + + /** 电池剩余时间 */ + private Integer batteryRemainTime; + + /** 温度 */ + private Double temperature; + + /** 是否旁路运行 */ + private boolean bypassActive; + + /** 是否关机中 */ + private boolean shutdownActive; + + /** 是否输出开启 */ + private boolean outputOn; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryView.java b/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryView.java new file mode 100644 index 0000000..6591ef3 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsHistoryView.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.system.vo.ups; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * UPS 历史视图 + * + * @author Codex + * @since 2026-04-07 + */ +@Data +public class UpsHistoryView { + + /** 服务端当前时间 */ + private long serverTime; + + /** 采样周期 */ + private long sampleRateMs; + + /** 查询开始时间 */ + private long from; + + /** 查询结束时间 */ + private long to; + + /** 历史点 */ + private List points = new ArrayList<>(); +} diff --git a/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsStatusView.java b/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsStatusView.java new file mode 100644 index 0000000..23047f5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/system/vo/ups/UpsStatusView.java @@ -0,0 +1,94 @@ +package com.imyeyu.api.modules.system.vo.ups; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * UPS 状态视图 + * + * @author Codex + * @since 2026-04-07 + */ +@Data +public class UpsStatusView { + + /** 服务端当前时间 */ + private long serverTime; + + /** UPS 数据时间 */ + private Long upsTime; + + /** 上游主机地址 */ + private String hostName; + + /** 厂商名称 */ + private String customer; + + /** 上游版本 */ + private String version; + + /** 设备标识 */ + private String deviceId; + + /** UPS 类型 */ + private String upsType; + + /** UPS 形态 */ + private String morphological; + + /** 输入输出相位 */ + private String ioPhase; + + /** 工作模式 */ + private String workMode; + + /** 输入电压 */ + private Double inputVoltage; + + /** 输入频率 */ + private Double inputFrequency; + + /** 输出电压 */ + private Double outputVoltage; + + /** 输出频率 */ + private Double outputFrequency; + + /** 输出负载百分比 */ + private Integer outputLoadPercent; + + /** 电池电压 */ + private Double batteryVoltage; + + /** 电池容量百分比 */ + private Integer batteryCapacity; + + /** 电池剩余时间 */ + private Integer batteryRemainTime; + + /** 温度 */ + private Double temperature; + + /** 是否旁路运行 */ + private boolean bypassActive; + + /** 是否关机中 */ + private boolean shutdownActive; + + /** 是否输出开启 */ + private boolean outputOn; + + /** 是否充电中 */ + private boolean charging; + + /** 故障类型 */ + private String faultType; + + /** 故障明细 */ + private String faultKind; + + /** 告警列表 */ + private List warnings = new ArrayList<>(); +}