diff --git a/agent/agent b/agent/agent
index a22af08..02ac3f0 100755
Binary files a/agent/agent and b/agent/agent differ
diff --git a/agent/agent.json b/agent/agent.json
index 98e721e..dc8aa80 100644
--- a/agent/agent.json
+++ b/agent/agent.json
@@ -1,9 +1,9 @@
{
- "server_url": "http://localhost:8080/api",
- "id": "yunc",
- "name": "cloud",
- "device_id": "yunc",
- "token": "f1dee2c8ffbdd4974af84b92a254892b",
+ "server_url": "http://10.35.10.12:8080/api",
+ "id": "agent-fnos1",
+ "name": "fnos1",
+ "device_id": "agent-fnos1",
+ "token": "eea3ffc9b3bb6b2a9f2e5bf228a2c7db",
"interval": "10s",
"debug": true,
"api_port": 8081
diff --git a/agent/config.json b/agent/config.json
new file mode 100644
index 0000000..cf9dbf6
--- /dev/null
+++ b/agent/config.json
@@ -0,0 +1,23 @@
+{
+ "server": {
+ "port": 8080
+ },
+ "influxdb": {
+ "url": "http://localhost:8086",
+ "token": "",
+ "org": "monitor",
+ "bucket": "monitor",
+ "username": "",
+ "password": ""
+ },
+ "db": {
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "username": "root",
+ "password": "",
+ "database": "monitor",
+ "ssl_mode": "disable",
+ "charset": "utf8mb4"
+ }
+}
\ No newline at end of file
diff --git a/agent/main.go b/agent/main.go
index 8cb98bf..9f237c1 100644
--- a/agent/main.go
+++ b/agent/main.go
@@ -5,8 +5,10 @@ import (
"encoding/json"
"fmt"
"log"
+ stdnet "net"
"net/http"
"os"
+ "sort"
"strconv"
"sync"
"time"
@@ -15,6 +17,7 @@ import (
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
+ "github.com/shirou/gopsutil/process"
)
// Config Agent配置
@@ -43,13 +46,56 @@ type DiskMetrics struct {
Total uint64 `json:"total"` // 总容量 (bytes)
}
+// ProcessMetrics 进程监控指标
+type ProcessMetrics struct {
+ Name string `json:"name"` // 进程名
+ Username string `json:"username"` // 用户名
+ PID int32 `json:"pid"` // 进程ID
+ CPU float64 `json:"cpu"` // CPU使用率
+ Memory float64 `json:"memory"` // 内存使用率
+ Path string `json:"path"` // 路径
+ Cmdline string `json:"cmdline"` // 命令行
+ Ports []int `json:"ports"` // 占用端口
+}
+
+// DiskDetailMetrics 磁盘详细信息
+type DiskDetailMetrics struct {
+ DeviceID string `json:"device_id"` // 设备ID
+ Status string `json:"status"` // 设备状态
+ Type string `json:"type"` // 设备类型
+ SizeGB float64 `json:"size_gb"` // 设备大小(GB)
+ Model string `json:"model"` // 设备型号
+ InterfaceType string `json:"interface_type"` // 接口类型
+ Description string `json:"description"` // 设备描述
+}
+
+// LogEntry 系统日志条目
+type LogEntry struct {
+ Sequence int `json:"sequence"` // 日志序号
+ Source string `json:"source"` // 来源
+ Time time.Time `json:"time"` // 发生时间
+ Message string `json:"message"` // 内容
+}
+
// Metrics 监控指标
type Metrics struct {
- CPU float64 `json:"cpu"`
- CPUHz float64 `json:"cpu_hz"` // CPU频率 (MHz)
- Memory float64 `json:"memory"`
- Disk map[string]DiskMetrics `json:"disk"`
- Network map[string]NetworkInterfaceMetrics `json:"network"`
+ CPU float64 `json:"cpu"`
+ CPUHz float64 `json:"cpu_hz"` // CPU频率 (MHz)
+ Memory float64 `json:"memory"`
+ Disk map[string]DiskMetrics `json:"disk"`
+ DiskDetails []DiskDetailMetrics `json:"disk_details"` // 磁盘详细信息
+ Network map[string]NetworkInterfaceMetrics `json:"network"`
+ Processes []ProcessMetrics `json:"processes"` // 进程信息
+ Logs []LogEntry `json:"logs"` // 系统日志
+ RxTotal uint64 `json:"rx_total"` // 所有网卡累计接收字节数总和
+ TxTotal uint64 `json:"tx_total"` // 所有网卡累计发送字节数总和
+ RxRate uint64 `json:"rx_rate"` // 所有网卡实时接收速率总和 (bytes/s)
+ TxRate uint64 `json:"tx_rate"` // 所有网卡实时发送速率总和 (bytes/s)
+ // 设备信息字段
+ DeviceID string `json:"device_id"` // 设备ID
+ AgentID string `json:"agent_id"` // Agent唯一标识
+ Name string `json:"name"` // 设备名称
+ IP string `json:"ip"` // 设备IP地址
}
// 全局配置
@@ -78,6 +124,60 @@ func init() {
metricsBuffer = make([]*Metrics, 0)
}
+// getLocalIP 获取本机IP地址
+func getLocalIP() string {
+ // 获取所有网络接口
+ interfaces, err := stdnet.Interfaces()
+ if err != nil {
+ log.Printf("Failed to get network interfaces: %v", err)
+ return ""
+ }
+
+ // 遍历网络接口查找非回环、UP状态的IP
+ for _, iface := range interfaces {
+ // 跳过回环接口和非UP状态的接口
+ if iface.Flags&stdnet.FlagLoopback != 0 || iface.Flags&stdnet.FlagUp == 0 {
+ continue
+ }
+
+ // 获取接口的IP地址
+ addresses, err := iface.Addrs()
+ if err != nil {
+ log.Printf("Failed to get addresses for interface %s: %v", iface.Name, err)
+ continue
+ }
+
+ // 遍历地址并返回IPv4地址
+ for _, addr := range addresses {
+ var ip stdnet.IP
+ switch v := addr.(type) {
+ case *stdnet.IPNet:
+ ip = v.IP
+ case *stdnet.IPAddr:
+ ip = v.IP
+ }
+
+ // 跳过IPv6地址和回环地址
+ if ip == nil || ip.IsLoopback() || ip.To4() == nil {
+ continue
+ }
+
+ return ip.String()
+ }
+ }
+
+ // 如果找不到合适的IP,尝试另一种方法
+ conn, err := stdnet.Dial("udp", "8.8.8.8:80")
+ if err != nil {
+ log.Printf("Failed to dial UDP: %v", err)
+ return ""
+ }
+ defer conn.Close()
+
+ localAddr := conn.LocalAddr().(*stdnet.UDPAddr)
+ return localAddr.IP.String()
+}
+
// 初始化配置
func initConfig() {
// 默认配置
@@ -353,25 +453,27 @@ func collectDisk() (map[string]DiskMetrics, error) {
}
// 采集网络流量
-func collectNetwork() (map[string]NetworkInterfaceMetrics, error) {
+func collectNetwork() (map[string]NetworkInterfaceMetrics, uint64, uint64, uint64, uint64, error) {
// 获取所有网卡的统计数据
ioCounters, err := net.IOCounters(true)
if err != nil {
- return nil, err
+ // 当获取网卡数据失败时,返回空map和0值
+ return make(map[string]NetworkInterfaceMetrics), 0, 0, 0, 0, nil
}
- if len(ioCounters) == 0 {
- return make(map[string]NetworkInterfaceMetrics), nil
- }
+ // 初始化返回值
+ networkMetrics := make(map[string]NetworkInterfaceMetrics)
+ var totalRxBytes, totalTxBytes, totalRxRate, totalTxRate uint64
// 获取当前时间
currentTime := time.Now()
- // 初始化返回值
- networkMetrics := make(map[string]NetworkInterfaceMetrics)
-
// 遍历所有网卡
for _, counter := range ioCounters {
+ // 跳过空名称的网卡
+ if counter.Name == "" {
+ continue
+ }
// 获取当前网卡的累计流量
currentBytesSent := counter.BytesSent
currentBytesReceived := counter.BytesRecv
@@ -409,23 +511,265 @@ func collectNetwork() (map[string]NetworkInterfaceMetrics, error) {
TxBytes: currentBytesSent,
RxBytes: currentBytesReceived,
}
+
+ // 累加总流量
+ totalRxBytes += currentBytesReceived
+ totalTxBytes += currentBytesSent
+ totalRxRate += bytesReceivedRate
+ totalTxRate += bytesSentRate
}
// 更新上一次采集时间
lastCollectTime = currentTime
- // 返回所有网卡的速率和累计流量
- return networkMetrics, nil
+ // 返回所有网卡的速率和累计流量,以及总和
+ return networkMetrics, totalRxBytes, totalTxBytes, totalRxRate, totalTxRate, nil
}
// 采集所有监控指标
+// 采集进程信息,返回CPU使用率较高的前N个进程
+func collectProcessMetrics() ([]ProcessMetrics, error) {
+ // 只采集CPU使用率较高的前20个进程,避免性能问题
+ const maxProcesses = 20
+
+ // 获取所有进程ID
+ pids, err := process.Pids()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get process IDs: %w", err)
+ }
+
+ // 创建进程信息切片
+ processes := make([]ProcessMetrics, 0, maxProcesses)
+
+ // 用于并发采集进程信息
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ errCh := make(chan error, len(pids))
+
+ // 限制并发数量
+ concurrencyLimit := 10
+ semaphore := make(chan struct{}, concurrencyLimit)
+
+ for _, pid := range pids {
+ wg.Add(1)
+
+ // 控制并发数量
+ semaphore <- struct{}{}
+
+ go func(pid int32) {
+ defer wg.Done()
+ defer func() { <-semaphore }()
+
+ // 获取进程信息
+ p, err := process.NewProcess(pid)
+ if err != nil {
+ errCh <- nil // 忽略无法访问的进程
+ return
+ }
+
+ // 获取进程名
+ name, err := p.Name()
+ if err != nil {
+ errCh <- nil
+ return
+ }
+
+ // 获取用户名
+ username := ""
+ if uids, err := p.Uids(); err == nil && len(uids) > 0 {
+ // 简单实现,实际需要映射UID到用户名
+ username = strconv.Itoa(int(uids[0]))
+ }
+
+ // 获取CPU使用率
+ cpuPercent, err := p.CPUPercent()
+ if err != nil {
+ errCh <- nil
+ return
+ }
+
+ // 获取内存使用率
+ memInfo, err := p.MemoryInfo()
+ if err != nil {
+ errCh <- nil
+ return
+ }
+
+ // 获取系统总内存
+ vmStat, err := mem.VirtualMemory()
+ if err != nil {
+ errCh <- nil
+ return
+ }
+
+ // 计算内存使用率百分比
+ memPercent := float64(memInfo.RSS) / float64(vmStat.Total) * 100
+
+ // 获取进程路径
+ path, err := p.Exe()
+ if err != nil {
+ path = ""
+ }
+
+ // 获取命令行
+ cmdline, err := p.Cmdline()
+ if err != nil {
+ cmdline = ""
+ }
+
+ // 获取占用端口
+ ports := []int{}
+ if connections, err := p.Connections(); err == nil {
+ for _, conn := range connections {
+ // 只添加监听或已建立连接的端口
+ if conn.Status == "LISTEN" || conn.Status == "ESTABLISHED" {
+ ports = append(ports, int(conn.Laddr.Port))
+ }
+ }
+ }
+
+ // 创建进程信息
+ procMetric := ProcessMetrics{
+ Name: name,
+ Username: username,
+ PID: pid,
+ CPU: cpuPercent,
+ Memory: memPercent,
+ Path: path,
+ Cmdline: cmdline,
+ Ports: ports,
+ }
+
+ // 添加到切片
+ mu.Lock()
+ processes = append(processes, procMetric)
+ mu.Unlock()
+
+ errCh <- nil
+ }(pid)
+ }
+
+ // 等待所有goroutine完成
+ wg.Wait()
+ close(errCh)
+
+ // 检查是否有错误
+ for err := range errCh {
+ if err != nil {
+ log.Printf("Warning: failed to collect process info: %v", err)
+ }
+ }
+
+ // 根据CPU使用率排序,取前N个
+ sort.Slice(processes, func(i, j int) bool {
+ return processes[i].CPU > processes[j].CPU
+ })
+
+ // 限制返回的进程数量
+ if len(processes) > maxProcesses {
+ processes = processes[:maxProcesses]
+ }
+
+ return processes, nil
+}
+
+// 采集磁盘详细信息
+func collectDiskDetails() ([]DiskDetailMetrics, error) {
+ // 获取所有挂载点信息
+ partitions, err := disk.Partitions(false)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get disk partitions: %w", err)
+ }
+
+ // 创建磁盘详细信息切片
+ diskDetails := make([]DiskDetailMetrics, 0, len(partitions))
+
+ for _, partition := range partitions {
+ // 获取磁盘使用情况
+ usage, err := disk.Usage(partition.Mountpoint)
+ if err != nil {
+ continue // 忽略无法访问的分区
+ }
+
+ // 简单实现,获取设备ID
+ deviceID := partition.Device
+ if len(deviceID) > 0 && deviceID[0] == '/' {
+ deviceID = deviceID[1:]
+ }
+
+ // 设备状态
+ status := "online"
+
+ // 设备类型
+ diskType := "unknown"
+ if partition.Fstype != "" {
+ diskType = partition.Fstype
+ }
+
+ // 设备大小(GB)
+ sizeGB := float64(usage.Total) / (1024 * 1024 * 1024)
+
+ // 设备型号 - 简化实现,实际需要更复杂的逻辑
+ model := partition.Device
+
+ // 接口类型 - 简化实现
+ interfaceType := "unknown"
+ if len(partition.Device) > 0 {
+ if partition.Device[:3] == "sda" || partition.Device[:3] == "sdb" {
+ interfaceType = "SATA"
+ } else if partition.Device[:3] == "nvme" {
+ interfaceType = "NVMe"
+ } else if partition.Device[:3] == "mmc" {
+ interfaceType = "MMC"
+ } else if partition.Device[:3] == "vda" || partition.Device[:3] == "vdb" {
+ interfaceType = "Virtual"
+ }
+ }
+
+ // 设备描述
+ description := fmt.Sprintf("%s (%s)", partition.Device, partition.Fstype)
+
+ // 创建磁盘详细信息
+ diskDetail := DiskDetailMetrics{
+ DeviceID: deviceID,
+ Status: status,
+ Type: diskType,
+ SizeGB: sizeGB,
+ Model: model,
+ InterfaceType: interfaceType,
+ Description: description,
+ }
+
+ diskDetails = append(diskDetails, diskDetail)
+ }
+
+ return diskDetails, nil
+}
+
func collectMetrics() (*Metrics, error) {
metrics := &Metrics{}
+ // 初始化Network字段为非nil,避免空指针问题
+ metrics.Network = make(map[string]NetworkInterfaceMetrics)
+
+ // 设置设备信息
+ deviceID := config.DeviceID
+ if deviceID == "" {
+ deviceID = config.ID
+ }
+ metrics.DeviceID = deviceID
+ metrics.AgentID = config.ID
+ metrics.Name = config.Name
+ // 尝试获取本机IP地址
+ metrics.IP = getLocalIP()
+
// 采集CPU使用率和频率
cpuUsage, cpuHz, err := collectCPU()
if err != nil {
- return nil, fmt.Errorf("failed to collect CPU metrics: %w", err)
+ // CPU采集失败时使用0值
+ log.Printf("Failed to collect CPU metrics: %v, using 0 values", err)
+ cpuUsage = 0
+ cpuHz = 0
}
metrics.CPU = cpuUsage
metrics.CPUHz = cpuHz
@@ -433,24 +777,53 @@ func collectMetrics() (*Metrics, error) {
// 采集内存使用率
memoryUsage, err := collectMemory()
if err != nil {
- return nil, fmt.Errorf("failed to collect memory metrics: %w", err)
+ // 内存采集失败时使用0值
+ log.Printf("Failed to collect memory metrics: %v, using 0 value", err)
+ memoryUsage = 0
}
metrics.Memory = memoryUsage
// 采集磁盘使用率和总容量
diskMetricsMap, err := collectDisk()
if err != nil {
- return nil, fmt.Errorf("failed to collect disk metrics: %w", err)
+ // 磁盘采集失败时使用空map
+ log.Printf("Failed to collect disk metrics: %v, using empty map", err)
+ diskMetricsMap = make(map[string]DiskMetrics)
}
metrics.Disk = diskMetricsMap
- // 采集网络流量
- networkMetrics, err := collectNetwork()
+ // 采集磁盘详细信息
+ diskDetails, err := collectDiskDetails()
if err != nil {
- return nil, fmt.Errorf("failed to collect network metrics: %w", err)
+ // 磁盘详细信息采集失败时使用空切片
+ log.Printf("Failed to collect disk details: %v, using empty slice", err)
+ diskDetails = make([]DiskDetailMetrics, 0)
+ }
+ metrics.DiskDetails = diskDetails
+
+ // 采集进程信息
+ processes, err := collectProcessMetrics()
+ if err != nil {
+ // 进程信息采集失败时使用空切片
+ log.Printf("Failed to collect process metrics: %v, using empty slice", err)
+ processes = make([]ProcessMetrics, 0)
+ }
+ metrics.Processes = processes
+
+ // 采集网络流量
+ networkMetrics, rxTotal, txTotal, rxRate, txRate, err := collectNetwork()
+ if err != nil {
+ // 网络采集失败时使用0值(实际上collectNetwork已经处理了错误情况)
+ log.Printf("Failed to collect network metrics: %v, using 0 values", err)
+ networkMetrics = make(map[string]NetworkInterfaceMetrics)
+ rxTotal, txTotal, rxRate, txRate = 0, 0, 0, 0
}
// 直接使用采集到的网卡流量
metrics.Network = networkMetrics
+ metrics.RxTotal = rxTotal
+ metrics.TxTotal = txTotal
+ metrics.RxRate = rxRate
+ metrics.TxRate = txRate
return metrics, nil
}
diff --git a/backend/backend b/backend/backend
new file mode 100755
index 0000000..8a2e1c2
Binary files /dev/null and b/backend/backend differ
diff --git a/backend/config.json b/backend/config.json
index c9de2b4..8306f57 100644
--- a/backend/config.json
+++ b/backend/config.json
@@ -3,7 +3,7 @@
"port": 8080
},
"influxdb": {
- "url": "http://10.35.10.130:8066",
+ "url": "http://10.35.10.70:8066",
"token": "aVI5qMGz6e8d4pfyhVZNYfS5we7C8Bb-5bi-V7LpL3K6CmQyudauigoxDFv1UFo2Hvda7swKEqTe8eP6xy4jBw==",
"org": "AMAZEHOME",
"bucket": "AMAZEHOME",
diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go
index be7137d..a0d4d77 100644
--- a/backend/internal/handler/handler.go
+++ b/backend/internal/handler/handler.go
@@ -41,6 +41,8 @@ func RegisterRoutes(r *gin.Engine) {
metrics.GET("/memory", GetMemoryMetrics)
metrics.GET("/disk", GetDiskMetrics)
metrics.GET("/network", GetNetworkMetrics)
+ metrics.GET("/processes", GetProcessMetrics) // 添加进程信息查询端点
+ metrics.GET("/disk_details", GetDiskDetails) // 添加磁盘详细信息查询端点
// 添加POST端点,接收Agent发送的指标数据
metrics.POST("/", HandleMetricsPost)
}
@@ -73,6 +75,29 @@ type DiskMetrics struct {
Total uint64 `json:"total"` // 总容量 (bytes)
}
+// ProcessMetrics 进程监控指标
+type ProcessMetrics struct {
+ Name string `json:"name"` // 进程名
+ Username string `json:"username"` // 用户名
+ PID int32 `json:"pid"` // 进程ID
+ CPU float64 `json:"cpu"` // CPU使用率
+ Memory float64 `json:"memory"` // 内存使用率
+ Path string `json:"path"` // 路径
+ Cmdline string `json:"cmdline"` // 命令行
+ Ports []int `json:"ports"` // 占用端口
+}
+
+// DiskDetailMetrics 磁盘详细信息
+type DiskDetailMetrics struct {
+ DeviceID string `json:"device_id"` // 设备ID
+ Status string `json:"status"` // 设备状态
+ Type string `json:"type"` // 设备类型
+ SizeGB float64 `json:"size_gb"` // 设备大小(GB)
+ Model string `json:"model"` // 设备型号
+ InterfaceType string `json:"interface_type"` // 接口类型
+ Description string `json:"description"` // 设备描述
+}
+
// NetworkInterfaceMetrics 网卡监控指标
type NetworkInterfaceMetrics struct {
BytesSent uint64 `json:"bytes_sent"` // 发送速率 (bytes/s)
@@ -83,11 +108,17 @@ type NetworkInterfaceMetrics struct {
// MetricsRequest 指标请求结构
type MetricsRequest struct {
- CPU float64 `json:"cpu"`
- CPUHz float64 `json:"cpu_hz"` // CPU频率 (MHz)
- Memory float64 `json:"memory"`
- Disk map[string]DiskMetrics `json:"disk"`
- Network map[string]NetworkInterfaceMetrics `json:"network"`
+ CPU float64 `json:"cpu"`
+ CPUHz float64 `json:"cpu_hz"` // CPU频率 (MHz)
+ Memory float64 `json:"memory"`
+ Disk map[string]DiskMetrics `json:"disk"`
+ DiskDetails []DiskDetailMetrics `json:"disk_details"` // 磁盘详细信息
+ Network map[string]NetworkInterfaceMetrics `json:"network"`
+ Processes []ProcessMetrics `json:"processes"` // 进程信息
+ RxTotal uint64 `json:"rx_total"` // 所有网卡累计接收字节数总和
+ TxTotal uint64 `json:"tx_total"` // 所有网卡累计发送字节数总和
+ RxRate uint64 `json:"rx_rate"` // 所有网卡实时接收速率总和 (bytes/s)
+ TxRate uint64 `json:"tx_rate"` // 所有网卡实时发送速率总和 (bytes/s)
}
// HandleMetricsPost 处理Agent发送的指标数据
@@ -162,24 +193,28 @@ func HandleMetricsPost(c *gin.Context) {
metricsList = append(metricsList, singleMetric)
}
+ // 创建单独的上下文用于InfluxDB写入,避免HTTP请求结束时上下文被取消
+ writeCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
// 处理所有指标
for i, req := range metricsList {
// 写入CPU使用率指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "cpu", req.CPU, baseTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "cpu", req.CPU, baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write CPU metrics: %v", err)
}
// 写入CPU频率指标(如果有)
if req.CPUHz > 0 {
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "cpu_hz", req.CPUHz, baseTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "cpu_hz", req.CPUHz, baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write CPU Hz metrics: %v", err)
}
}
// 写入内存指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "memory", req.Memory, baseTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "memory", req.Memory, baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write memory metrics: %v", err)
}
@@ -196,7 +231,7 @@ func HandleMetricsPost(c *gin.Context) {
tags["mountpoint"] = mountpoint
// 写入磁盘使用率指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "disk", diskMetrics.UsedPercent, tags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "disk", diskMetrics.UsedPercent, tags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write disk metrics for mountpoint %s: %v", mountpoint, err)
}
@@ -206,6 +241,10 @@ func HandleMetricsPost(c *gin.Context) {
var totalBytesSent, totalBytesReceived uint64
var totalTxBytes, totalRxBytes uint64 // 累计总流量
for interfaceName, networkMetrics := range req.Network {
+ // 跳过空名称的网卡
+ if interfaceName == "" {
+ continue
+ }
// 为每个网卡创建标签,包含基础标签和网卡名称
interfaceTags := make(map[string]string)
// 复制基础标签
@@ -216,25 +255,25 @@ func HandleMetricsPost(c *gin.Context) {
interfaceTags["interface"] = interfaceName
// 写入网络发送速率指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "network_sent", float64(networkMetrics.BytesSent), interfaceTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "network_sent", float64(networkMetrics.BytesSent), interfaceTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write network sent metrics for interface %s: %v", interfaceName, err)
}
// 写入网络接收速率指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "network_received", float64(networkMetrics.BytesReceived), interfaceTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "network_received", float64(networkMetrics.BytesReceived), interfaceTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write network received metrics for interface %s: %v", interfaceName, err)
}
// 写入累计发送字节数指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "network_tx_bytes", float64(networkMetrics.TxBytes), interfaceTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "network_tx_bytes", float64(networkMetrics.TxBytes), interfaceTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write network tx_bytes metrics for interface %s: %v", interfaceName, err)
}
// 写入累计接收字节数指标
- if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "network_rx_bytes", float64(networkMetrics.RxBytes), interfaceTags); err != nil {
+ if err := globalStorage.WriteMetric(writeCtx, deviceID, "network_rx_bytes", float64(networkMetrics.RxBytes), interfaceTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write network rx_bytes metrics for interface %s: %v", interfaceName, err)
}
@@ -248,6 +287,22 @@ func HandleMetricsPost(c *gin.Context) {
totalRxBytes += networkMetrics.RxBytes
}
+ // 写入进程信息
+ for _, proc := range req.Processes {
+ if err := globalStorage.WriteProcessMetric(writeCtx, deviceID, proc.Name, proc.Username, proc.PID, proc.CPU, proc.Memory, proc.Path, proc.Cmdline, proc.Ports, baseTags); err != nil {
+ // 只记录警告,不影响后续指标处理
+ log.Printf("Warning: Failed to write process metrics for PID %d: %v", proc.PID, err)
+ }
+ }
+
+ // 写入磁盘详细信息
+ for _, diskDetail := range req.DiskDetails {
+ if err := globalStorage.WriteDiskDetailMetric(writeCtx, deviceID, diskDetail.DeviceID, diskDetail.Status, diskDetail.Type, diskDetail.SizeGB, diskDetail.Model, diskDetail.InterfaceType, diskDetail.Description, baseTags); err != nil {
+ // 只记录警告,不影响后续指标处理
+ log.Printf("Warning: Failed to write disk details for device %s: %v", diskDetail.DeviceID, err)
+ }
+ }
+
// 广播指标更新消息,只广播最后一个指标
if i == len(metricsList)-1 {
// 准备广播的磁盘使用率数据(兼容旧格式)
@@ -412,7 +467,7 @@ func GetCPUMetrics(c *gin.Context) {
}
// 处理数据,传递interval、startTime和endTime参数
- processedData := ProcessMetrics(points, aggregation, interval, startTime, endTime)
+ processedData := ProcessMetricData(points, aggregation, interval, startTime, endTime)
c.JSON(http.StatusOK, gin.H{
"data": processedData,
@@ -440,7 +495,7 @@ func GetMemoryMetrics(c *gin.Context) {
}
// 处理数据,传递interval、startTime和endTime参数
- processedData := ProcessMetrics(points, aggregation, interval, startTime, endTime)
+ processedData := ProcessMetricData(points, aggregation, interval, startTime, endTime)
c.JSON(http.StatusOK, gin.H{
"data": processedData,
@@ -481,7 +536,7 @@ func GetDiskMetrics(c *gin.Context) {
// 处理数据,为每个挂载点创建独立的数据集
result := make(map[string][]MetricData)
for mountpoint, mountpointPoints := range mountpointData {
- processedData := ProcessMetrics(mountpointPoints, aggregation, interval, startTime, endTime)
+ processedData := ProcessMetricData(mountpointPoints, aggregation, interval, startTime, endTime)
result[mountpoint] = processedData
}
@@ -499,10 +554,14 @@ func GetNetworkMetrics(c *gin.Context) {
aggregation := c.DefaultQuery("aggregation", "average")
interval := c.DefaultQuery("interval", "10s") // 添加interval参数,默认10秒
- // 查询发送和接收的网络指标
+ // 查询发送和接收的网络速率指标
sentPoints, err1 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_sent", startTime, endTime)
receivedPoints, err2 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_received", startTime, endTime)
+ // 查询发送和接收的累积总流量指标
+ txBytesPoints, err3 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_total_tx_bytes", startTime, endTime)
+ rxBytesPoints, err4 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_total_rx_bytes", startTime, endTime)
+
// 处理错误
if err1 != nil {
log.Printf("Warning: Failed to query network sent metrics: %v", err1)
@@ -512,12 +571,24 @@ func GetNetworkMetrics(c *gin.Context) {
log.Printf("Warning: Failed to query network received metrics: %v", err2)
receivedPoints = []storage.MetricPoint{}
}
+ if err3 != nil {
+ log.Printf("Warning: Failed to query network_total_tx_bytes metrics: %v", err3)
+ txBytesPoints = []storage.MetricPoint{}
+ }
+ if err4 != nil {
+ log.Printf("Warning: Failed to query network_total_rx_bytes metrics: %v", err4)
+ rxBytesPoints = []storage.MetricPoint{}
+ }
- // 按网卡名称分组发送和接收的指标
+ // 按网卡名称分组发送和接收的速率指标
sentByInterface := make(map[string][]storage.MetricPoint)
receivedByInterface := make(map[string][]storage.MetricPoint)
- // 分组发送的网络指标
+ // 按网卡名称分组发送和接收的累积总流量指标
+ txBytesByInterface := make(map[string][]storage.MetricPoint)
+ rxBytesByInterface := make(map[string][]storage.MetricPoint)
+
+ // 分组发送的网络速率指标
for _, point := range sentPoints {
// 获取网卡名称,默认使用"all"表示所有网卡
interfaceName := point.Tags["interface"]
@@ -527,7 +598,7 @@ func GetNetworkMetrics(c *gin.Context) {
sentByInterface[interfaceName] = append(sentByInterface[interfaceName], point)
}
- // 分组接收的网络指标
+ // 分组接收的网络速率指标
for _, point := range receivedPoints {
// 获取网卡名称,默认使用"all"表示所有网卡
interfaceName := point.Tags["interface"]
@@ -537,6 +608,26 @@ func GetNetworkMetrics(c *gin.Context) {
receivedByInterface[interfaceName] = append(receivedByInterface[interfaceName], point)
}
+ // 分组发送的累积总流量指标
+ for _, point := range txBytesPoints {
+ // 获取网卡名称,默认使用"all"表示所有网卡
+ interfaceName := point.Tags["interface"]
+ if interfaceName == "" {
+ interfaceName = "all"
+ }
+ txBytesByInterface[interfaceName] = append(txBytesByInterface[interfaceName], point)
+ }
+
+ // 分组接收的累积总流量指标
+ for _, point := range rxBytesPoints {
+ // 获取网卡名称,默认使用"all"表示所有网卡
+ interfaceName := point.Tags["interface"]
+ if interfaceName == "" {
+ interfaceName = "all"
+ }
+ rxBytesByInterface[interfaceName] = append(rxBytesByInterface[interfaceName], point)
+ }
+
// 处理数据,为每个网卡创建独立的数据集
result := make(map[string]map[string][]MetricData)
@@ -548,21 +639,37 @@ func GetNetworkMetrics(c *gin.Context) {
for iface := range receivedByInterface {
allInterfaces[iface] = true
}
+ for iface := range txBytesByInterface {
+ allInterfaces[iface] = true
+ }
+ for iface := range rxBytesByInterface {
+ allInterfaces[iface] = true
+ }
// 为每个网卡处理数据
for iface := range allInterfaces {
- // 获取该网卡的发送和接收指标
+ // 获取该网卡的速率指标
ifaceSentPoints := sentByInterface[iface]
ifaceReceivedPoints := receivedByInterface[iface]
- // 处理数据
- processedSentData := ProcessMetrics(ifaceSentPoints, aggregation, interval, startTime, endTime)
- processedReceivedData := ProcessMetrics(ifaceReceivedPoints, aggregation, interval, startTime, endTime)
+ // 获取该网卡的累积总流量指标
+ ifaceTxBytesPoints := txBytesByInterface[iface]
+ ifaceRxBytesPoints := rxBytesByInterface[iface]
+
+ // 处理速率数据
+ processedSentData := ProcessMetricData(ifaceSentPoints, aggregation, interval, startTime, endTime)
+ processedReceivedData := ProcessMetricData(ifaceReceivedPoints, aggregation, interval, startTime, endTime)
+
+ // 处理累积总流量数据
+ processedTxBytesData := ProcessMetricData(ifaceTxBytesPoints, aggregation, interval, startTime, endTime)
+ processedRxBytesData := ProcessMetricData(ifaceRxBytesPoints, aggregation, interval, startTime, endTime)
// 保存结果
result[iface] = map[string][]MetricData{
- "sent": processedSentData,
- "received": processedReceivedData,
+ "sent": processedSentData, // 发送速率数据
+ "received": processedReceivedData, // 接收速率数据
+ "tx_bytes": processedTxBytesData, // 发送累积总流量数据
+ "rx_bytes": processedRxBytesData, // 接收累积总流量数据
}
}
@@ -806,3 +913,49 @@ func GetAllDevices(c *gin.Context) {
"devices": devices,
})
}
+
+// GetProcessMetrics 获取进程指标
+func GetProcessMetrics(c *gin.Context) {
+ // 获取查询参数
+ deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
+ startTime := c.DefaultQuery("start_time", "-1h")
+ endTime := c.DefaultQuery("end_time", "now()")
+
+ // 查询数据
+ processes, err := globalStorage.QueryProcessMetrics(context.Background(), deviceID, startTime, endTime)
+ if err != nil {
+ // 只记录警告,返回空数据
+ log.Printf("Warning: Failed to query process metrics: %v", err)
+ c.JSON(http.StatusOK, gin.H{
+ "data": []map[string]interface{}{},
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "data": processes,
+ })
+}
+
+// GetDiskDetails 获取磁盘详细信息
+func GetDiskDetails(c *gin.Context) {
+ // 获取查询参数
+ deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
+ startTime := c.DefaultQuery("start_time", "-1h")
+ endTime := c.DefaultQuery("end_time", "now()")
+
+ // 查询数据
+ diskDetails, err := globalStorage.QueryDiskDetails(context.Background(), deviceID, startTime, endTime)
+ if err != nil {
+ // 只记录警告,返回空数据
+ log.Printf("Warning: Failed to query disk details: %v", err)
+ c.JSON(http.StatusOK, gin.H{
+ "data": []map[string]interface{}{},
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "data": diskDetails,
+ })
+}
diff --git a/backend/internal/handler/processor.go b/backend/internal/handler/processor.go
index 91b42ca..fa85c16 100644
--- a/backend/internal/handler/processor.go
+++ b/backend/internal/handler/processor.go
@@ -71,8 +71,8 @@ func FormatTimeByInterval(t time.Time, intervalSeconds int) string {
}
}
-// ProcessMetrics 处理监控数据,支持动态时间区间
-func ProcessMetrics(points []storage.MetricPoint, aggregation string, intervalStr string, startTime, endTime string) []MetricData {
+// ProcessMetricData 处理监控数据,支持动态时间区间
+func ProcessMetricData(points []storage.MetricPoint, aggregation string, intervalStr string, startTime, endTime string) []MetricData {
// 解析时间区间
intervalSeconds, err := ParseInterval(intervalStr)
if err != nil {
diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go
index bf66daa..37946d9 100644
--- a/backend/internal/storage/storage.go
+++ b/backend/internal/storage/storage.go
@@ -2,7 +2,9 @@ package storage
import (
"context"
+ "fmt"
"log"
+ "math/rand"
"strings"
"time"
@@ -10,6 +12,28 @@ import (
"github.com/monitor/backend/config"
)
+// formatTags 将标签映射格式化为InfluxDB行协议格式
+func formatTags(tags map[string]string) string {
+ var tagList []string
+ for k, v := range tags {
+ // 跳过空值的标签,避免InfluxDB解析错误
+ if v == "" {
+ continue
+ }
+ tagList = append(tagList, fmt.Sprintf("%s=%s", k, escapeTagValue(v)))
+ }
+ return strings.Join(tagList, ",")
+}
+
+// escapeTagValue 转义标签值中的特殊字符
+func escapeTagValue(value string) string {
+ // 替换逗号、空格和等号为转义后的形式
+ escaped := strings.ReplaceAll(value, ",", "\\,")
+ escaped = strings.ReplaceAll(escaped, " ", "\\ ")
+ escaped = strings.ReplaceAll(escaped, "=", "\\=")
+ return escaped
+}
+
// MetricPoint 自定义监控指标点
type MetricPoint struct {
Time time.Time `json:"time"`
@@ -39,8 +63,10 @@ func NewStorage(cfg *config.Config) *Storage {
client = influxdb2.NewClient(cfg.InfluxDB.URL, "")
}
+ // 配置InfluxDB客户端选项
+ options := client.Options()
// 禁用InfluxDB客户端的调试日志
- client.Options().SetLogLevel(0)
+ options.SetLogLevel(0)
return &Storage{
client: client,
@@ -54,10 +80,70 @@ func (s *Storage) Close() {
s.client.Close()
}
-// WriteMetric 写入监控指标
-func (s *Storage) WriteMetric(ctx context.Context, deviceID, metricType string, value float64, tags map[string]string) error {
- writeAPI := s.client.WriteAPIBlocking(s.org, s.bucket)
+// 写入数据到InfluxDB,带重试机制
+func (s *Storage) writeData(ctx context.Context, measurement string, tags map[string]string, fields map[string]interface{}, deviceID, metricType string) error {
+ // 重试配置 - 减少重试次数和延迟,确保在超时时间内完成
+ maxRetries := 2
+ baseDelay := 200 * time.Millisecond
+ for i := 0; i <= maxRetries; i++ {
+ // 如果上下文已取消,直接返回
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ // 写入数据点
+ writeAPI := s.client.WriteAPIBlocking(s.org, s.bucket)
+ // 构建行协议字符串
+ var fieldList []string
+ for k, v := range fields {
+ var fieldStr string
+ // 根据字段类型格式化
+ switch v := v.(type) {
+ case string:
+ fieldStr = fmt.Sprintf("%s=%q", k, v)
+ case float64, int, int32, int64:
+ fieldStr = fmt.Sprintf("%s=%v", k, v)
+ case bool:
+ fieldStr = fmt.Sprintf("%s=%t", k, v)
+ default:
+ // 转换为字符串
+ fieldStr = fmt.Sprintf("%s=%q", k, fmt.Sprintf("%v", v))
+ }
+ fieldList = append(fieldList, fieldStr)
+ }
+ line := fmt.Sprintf("%s,%s %s %d", measurement, formatTags(tags), strings.Join(fieldList, ","), time.Now().UnixNano())
+ err := writeAPI.WriteRecord(ctx, line)
+
+ if err == nil {
+ // 写入成功,直接返回
+ return nil
+ }
+
+ // 如果是最后一次重试,返回错误
+ if i == maxRetries {
+ return err
+ }
+
+ // 计算重试延迟(指数退避)
+ delay := baseDelay*time.Duration(1<= 5 {
+ break
+ }
+ portsStr = append(portsStr, fmt.Sprintf("%d", port))
+ }
+ allTags["ports"] = strings.Join(portsStr, ",")
+
+ // 创建字段映射
+ fields := map[string]interface{}{
+ "cpu_usage": cpu,
+ "memory_usage": memory,
+ "path": path,
+ "cmdline": cmdline,
+ }
+
+ // 使用新的writeData方法
+ return s.writeData(ctx, "processes", allTags, fields, deviceID, "process")
+}
+
+// WriteDiskDetailMetric 写入磁盘详细信息
+func (s *Storage) WriteDiskDetailMetric(ctx context.Context, deviceID, diskDeviceID, status, diskType string, sizeGB float64, model, interfaceType, description string, tags map[string]string) error {
+ // 创建标签映射,合并原有标签和新标签
+ allTags := make(map[string]string)
+ // 复制原有标签
+ for k, v := range tags {
+ allTags[k] = v
+ }
+ // 添加设备ID标签
+ allTags["device_id"] = deviceID
+ // 添加磁盘相关标签
+ allTags["disk_id"] = diskDeviceID
+ allTags["status"] = status
+ allTags["type"] = diskType
+ allTags["model"] = model
+ allTags["interface_type"] = interfaceType
+
+ // 创建字段映射
+ fields := map[string]interface{}{
+ "size_gb": sizeGB,
+ "description": description,
+ }
+
+ // 使用新的writeData方法
+ return s.writeData(ctx, "disk_details", allTags, fields, deviceID, "disk_detail")
}
// QueryMetrics 查询监控指标
@@ -315,3 +460,132 @@ func (s *Storage) QueryDeviceStatus(ctx context.Context, deviceID string) (strin
return agentName, status, nil
}
+
+// QueryProcessMetrics 查询进程指标
+func (s *Storage) QueryProcessMetrics(ctx context.Context, deviceID string, startTime, endTime string) ([]map[string]interface{}, error) {
+ queryAPI := s.client.QueryAPI(s.org)
+
+ // 构建查询语句
+ query := `from(bucket: "` + s.bucket + `")
+ |> range(start: ` + startTime + `, stop: ` + endTime + `)
+ |> filter(fn: (r) => r["_measurement"] == "processes")`
+
+ // 如果指定了设备ID,添加设备ID过滤
+ if deviceID != "" {
+ query += `
+ |> filter(fn: (r) => r["device_id"] == "` + deviceID + `")`
+ }
+
+ // 获取最新的进程数据
+ query += `
+ |> last()`
+
+ // 执行查询
+ queryResult, err := queryAPI.Query(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+ defer queryResult.Close()
+
+ // 存储进程数据
+ processes := make([]map[string]interface{}, 0)
+
+ // 处理查询结果
+ for queryResult.Next() {
+ if queryResult.TableChanged() {
+ // 表结构变化,跳过
+ continue
+ }
+
+ // 获取记录
+ record := queryResult.Record()
+
+ // 构建进程数据
+ processData := map[string]interface{}{
+ "time": record.Time(),
+ "device_id": record.ValueByKey("device_id"),
+ "process_name": record.ValueByKey("process_name"),
+ "username": record.ValueByKey("username"),
+ "pid": record.ValueByKey("pid"),
+ "cpu_usage": record.ValueByKey("cpu_usage"),
+ "memory_usage": record.ValueByKey("memory_usage"),
+ "path": record.ValueByKey("path"),
+ "cmdline": record.ValueByKey("cmdline"),
+ "ports": record.ValueByKey("ports"),
+ "agent_name": record.ValueByKey("agent_name"),
+ }
+
+ // 添加到进程列表
+ processes = append(processes, processData)
+ }
+
+ if queryResult.Err() != nil {
+ return nil, queryResult.Err()
+ }
+
+ return processes, nil
+}
+
+// QueryDiskDetails 查询磁盘详细信息
+func (s *Storage) QueryDiskDetails(ctx context.Context, deviceID string, startTime, endTime string) ([]map[string]interface{}, error) {
+ queryAPI := s.client.QueryAPI(s.org)
+
+ // 构建查询语句
+ query := `from(bucket: "` + s.bucket + `")
+ |> range(start: ` + startTime + `, stop: ` + endTime + `)
+ |> filter(fn: (r) => r["_measurement"] == "disk_details")`
+
+ // 如果指定了设备ID,添加设备ID过滤
+ if deviceID != "" {
+ query += `
+ |> filter(fn: (r) => r["device_id"] == "` + deviceID + `")`
+ }
+
+ // 获取最新的磁盘详细信息
+ query += `
+ |> last()`
+
+ // 执行查询
+ queryResult, err := queryAPI.Query(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+ defer queryResult.Close()
+
+ // 存储磁盘详细信息
+ diskDetails := make([]map[string]interface{}, 0)
+
+ // 处理查询结果
+ for queryResult.Next() {
+ if queryResult.TableChanged() {
+ // 表结构变化,跳过
+ continue
+ }
+
+ // 获取记录
+ record := queryResult.Record()
+
+ // 构建磁盘详细信息
+ diskData := map[string]interface{}{
+ "time": record.Time(),
+ "device_id": record.ValueByKey("device_id"),
+ "disk_id": record.ValueByKey("disk_id"),
+ "status": record.ValueByKey("status"),
+ "type": record.ValueByKey("type"),
+ "size_gb": record.ValueByKey("size_gb"),
+ "model": record.ValueByKey("model"),
+ "interface_type": record.ValueByKey("interface_type"),
+ "description": record.ValueByKey("description"),
+ "agent_name": record.ValueByKey("agent_name"),
+ }
+
+ // 添加到磁盘详细信息列表
+ diskDetails = append(diskDetails, diskData)
+ }
+
+ if queryResult.Err() != nil {
+ return nil, queryResult.Err()
+ }
+
+ return diskDetails, nil
+}
diff --git a/backend/monitor-server b/backend/monitor-server
index deecee2..8a2e1c2 100755
Binary files a/backend/monitor-server and b/backend/monitor-server differ
diff --git a/backend/static/index.html b/backend/static/index.html
index 28f7c2b..bfa344c 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -254,19 +254,20 @@
-
+
网络流量 接收: 0 MB/s | 发送: 0 MB/s 接收速率 0.00 MB | 发送速率 0.00 MB0.0
-
总量: 接收 0 MB | 发送 0 MB0.00 ↓
+
接收总量 0.00 MB | 发送总量 0.00 MB进程信息
+
+
+
+
+
+
+
+
+
+ 进程名
+ 用户名
+ 进程ID
+ CPU (%)
+ 内存 (%)
+ 路径
+ 命令行
+ 占用端口
+
` +
- `总量: 接收 ${formatBytes(receivedTotal)} | 发送 ${formatBytes(sentTotal)}`;
+ networkDetailsElement.textContent = `${rxRateMB} MB/s | ${txRateMB} MB/s`;
}
}
+
}
// 更新历史指标数据
@@ -1690,6 +1855,34 @@ function updateHistoryMetrics(metrics) {
// 更新图表数据
function updateCharts(cpuData, memoryData, diskData, networkData) {
+ // 保存待处理的图表更新数据
+ state.pendingChartUpdate = {
+ cpuData,
+ memoryData,
+ diskData,
+ networkData
+ };
+
+ // 使用节流机制更新图表
+ _updateChartsThrottled();
+}
+
+// 带节流功能的图表更新函数
+function _updateChartsThrottled() {
+ const now = Date.now();
+ if (now - state.lastChartUpdate < state.chartUpdateThrottle) {
+ // 如果距离上次更新时间不足节流时间,延迟执行
+ setTimeout(_updateChartsThrottled, state.chartUpdateThrottle - (now - state.lastChartUpdate));
+ return;
+ }
+
+ // 执行实际的图表更新
+ _updateCharts(state.pendingChartUpdate);
+ state.lastChartUpdate = now;
+}
+
+// 内部图表更新函数,执行实际的图表渲染
+function _updateCharts({ cpuData, memoryData, diskData, networkData }) {
// 数据点排序函数
const sortDataByTime = (data) => {
return [...data].sort((a, b) => {
@@ -1941,73 +2134,61 @@ function updateCharts(cpuData, memoryData, diskData, networkData) {
// 更新网络流量趋势图表(发送总和和接收总和)
if (networkData && charts.network) {
- let sentData, receivedData;
+ let txBytesData, rxBytesData;
// 如果是按网卡分组的数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
if (state.currentInterface === 'all') {
- // 计算所有网卡的发送总和
- sentData = sumAllInterfacesData(networkData, 'sent');
- // 计算所有网卡的接收总和
- receivedData = sumAllInterfacesData(networkData, 'received');
+ // 计算所有网卡的发送累积总流量
+ txBytesData = sumAllInterfacesData(networkData, 'tx_bytes');
+ // 计算所有网卡的接收累积总流量
+ rxBytesData = sumAllInterfacesData(networkData, 'rx_bytes');
} else {
// 选择当前选中的网卡数据
const selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
- sentData = selectedNetworkData.sent || [];
- receivedData = selectedNetworkData.received || [];
+ txBytesData = selectedNetworkData.tx_bytes || [];
+ rxBytesData = selectedNetworkData.rx_bytes || [];
}
} else {
// 直接使用数据
- sentData = networkData.sent || [];
- receivedData = networkData.received || [];
+ txBytesData = networkData.tx_bytes || [];
+ rxBytesData = networkData.rx_bytes || [];
}
- if (sentData.length > 0 || receivedData.length > 0) {
- // 计算发送总和(时间段内的累积值)
- if (Array.isArray(sentData) && sentData.length > 0) {
+ if (txBytesData.length > 0 || rxBytesData.length > 0) {
+ // 使用发送累积总流量数据
+ if (Array.isArray(txBytesData) && txBytesData.length > 0) {
// 排序发送数据
- const sortedSent = sortDataByTime(sentData);
+ const sortedTxBytes = sortDataByTime(txBytesData);
- // 计算累积发送总和(MB)
- let cumulativeSent = 0;
- const sentSumData = sortedSent.map(item => {
- // 转换为MB并累积
- const mbValue = item.value / (1024 * 1024);
- cumulativeSent += mbValue;
- return {
- time: item.time,
- value: cumulativeSent
- };
- });
+ // 转换为MB
+ const txBytesSumData = sortedTxBytes.map(item => ({
+ time: item.time,
+ value: item.value / (1024 * 1024) // 直接转换为MB
+ }));
// 使用固定份数X轴数据计算
- const fixedPointsSentSum = getFixedPointsData(sentSumData);
- charts.network.data.datasets[0].data = fixedPointsSentSum.map(item => ({
+ const fixedPointsTxBytesSum = getFixedPointsData(txBytesSumData);
+ charts.network.data.datasets[0].data = fixedPointsTxBytesSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
- // 计算接收总和(时间段内的累积值)
- if (Array.isArray(receivedData) && receivedData.length > 0) {
+ // 使用接收累积总流量数据
+ if (Array.isArray(rxBytesData) && rxBytesData.length > 0) {
// 排序接收数据
- const sortedReceived = sortDataByTime(receivedData);
+ const sortedRxBytes = sortDataByTime(rxBytesData);
- // 计算累积接收总和(MB)
- let cumulativeReceived = 0;
- const receivedSumData = sortedReceived.map(item => {
- // 转换为MB并累积
- const mbValue = item.value / (1024 * 1024);
- cumulativeReceived += mbValue;
- return {
- time: item.time,
- value: cumulativeReceived
- };
- });
+ // 转换为MB
+ const rxBytesSumData = sortedRxBytes.map(item => ({
+ time: item.time,
+ value: item.value / (1024 * 1024) // 直接转换为MB
+ }));
// 使用固定份数X轴数据计算
- const fixedPointsReceivedSum = getFixedPointsData(receivedSumData);
- charts.network.data.datasets[1].data = fixedPointsReceivedSum.map(item => ({
+ const fixedPointsRxBytesSum = getFixedPointsData(rxBytesSumData);
+ charts.network.data.datasets[1].data = fixedPointsRxBytesSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
@@ -2183,13 +2364,33 @@ function updateInterfaceDropdown(networkData) {
// 尝试重连WebSocket
function attemptReconnect() {
+ // 清除可能存在的重连定时器
+ if (wsReconnectTimeout) {
+ clearTimeout(wsReconnectTimeout);
+ wsReconnectTimeout = null;
+ }
+
if (wsReconnectAttempts < wsMaxReconnectAttempts) {
wsReconnectAttempts++;
- wsReconnectDelay *= 2;
- setTimeout(() => {
- console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts})`);
+ // 指数退避,但不超过最大延迟
+ wsReconnectDelay = Math.min(wsReconnectDelay * 2, wsMaxReconnectDelay);
+ // 添加随机抖动,防止多个客户端同时重连
+ const jitter = Math.random() * 1000;
+ const delay = wsReconnectDelay + jitter;
+
+ wsReconnectTimeout = setTimeout(() => {
+ console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts}),延迟 ${Math.round(delay)}ms`);
initWebSocket();
- }, wsReconnectDelay);
+ }, delay);
+ } else {
+ console.error('WebSocket重连失败,已达到最大重连次数');
+ // 30秒后重置重连状态,允许再次尝试
+ setTimeout(() => {
+ wsReconnectAttempts = 0;
+ wsReconnectDelay = 1000;
+ console.log('WebSocket重连状态已重置,准备再次尝试连接');
+ attemptReconnect();
+ }, 30000);
}
}
@@ -2520,91 +2721,133 @@ async function loadNetworkInterfaces() {
// 更新当前时间范围显示
const updateTimeRangeDisplay = () => {
+ let displayText = '';
switch(state.currentTimeRange) {
- case '30m':
- currentTimeRangeDisplay.textContent = '过去30分钟';
- break;
- case '1h':
- currentTimeRangeDisplay.textContent = '过去1小时';
- break;
- case '2h':
- currentTimeRangeDisplay.textContent = '过去2小时';
- break;
- case '6h':
- currentTimeRangeDisplay.textContent = '过去6小时';
- break;
- case '12h':
- currentTimeRangeDisplay.textContent = '过去12小时';
- break;
- case '24h':
- currentTimeRangeDisplay.textContent = '过去24小时';
- break;
- default:
- currentTimeRangeDisplay.textContent = '自定义时间范围';
- }
- };
-
- // 初始化显示
- updateTimeRangeDisplay();
-
- // 放大事件
- zoomInBtn.addEventListener('click', () => {
- // 只在使用预设时间范围时生效
- if (state.customStartTime && state.customEndTime) {
- // 使用自定义时间时,先清除自定义时间
- state.customStartTime = '';
- state.customEndTime = '';
- state.currentTimeRange = '1h';
- } else {
- // 查找当前时间范围在列表中的索引
- const currentIndex = timeRanges.indexOf(state.currentTimeRange);
- if (currentIndex > 0) {
- // 放大:使用更小的时间范围
- state.currentTimeRange = timeRanges[currentIndex - 1];
- }
- }
-
- // 更新显示
- updateTimeRangeDisplay();
- // 重新加载数据
- loadMetrics();
- });
-
- // 缩小事件
- zoomOutBtn.addEventListener('click', () => {
- // 只在使用预设时间范围时生效
- if (state.customStartTime && state.customEndTime) {
- // 使用自定义时间时,先清除自定义时间
- state.customStartTime = '';
- state.customEndTime = '';
- state.currentTimeRange = '1h';
- } else {
- // 查找当前时间范围在列表中的索引
- const currentIndex = timeRanges.indexOf(state.currentTimeRange);
- if (currentIndex < timeRanges.length - 1) {
- // 缩小:使用更大的时间范围
- state.currentTimeRange = timeRanges[currentIndex + 1];
- }
- }
-
- // 更新显示
- updateTimeRangeDisplay();
- // 重新加载数据
- loadMetrics();
- });
-
-
- // 重置缩放按钮事件处理
- if (resetZoomBtn) {
- resetZoomBtn.addEventListener('click', () => {
- // 重置所有图表的缩放
- Object.values(charts).forEach(chart => {
- if (chart && typeof chart.resetZoom === 'function') {
- chart.resetZoom();
- }
- });
- });
+ case '30m':
+ displayText = '过去30分钟';
+ break;
+ case '1h':
+ displayText = '过去1小时';
+ break;
+ case '2h':
+ displayText = '过去2小时';
+ break;
+ case '6h':
+ displayText = '过去6小时';
+ break;
+ case '12h':
+ displayText = '过去12小时';
+ break;
+ case '24h':
+ displayText = '过去24小时';
+ break;
+ default:
+ displayText = '自定义时间范围';
}
+
+ // 更新所有图表的时间范围显示
+ const displays = [
+ document.getElementById('cpuCurrentTimeRangeDisplay'),
+ document.getElementById('currentTimeRangeDisplay'),
+ document.getElementById('diskCurrentTimeRangeDisplay'),
+ document.getElementById('networkCurrentTimeRangeDisplay'),
+ document.getElementById('speedCurrentTimeRangeDisplay')
+ ];
+
+ displays.forEach(display => {
+ if (display) display.textContent = displayText;
+ });
+};
+
+// 初始化显示
+updateTimeRangeDisplay();
+
+// 放大事件
+const zoomInHandler = () => {
+ // 只在使用预设时间范围时生效
+ if (state.customStartTime && state.customEndTime) {
+ // 使用自定义时间时,先清除自定义时间
+ state.customStartTime = '';
+ state.customEndTime = '';
+ state.currentTimeRange = '1h';
+ } else {
+ // 查找当前时间范围在列表中的索引
+ const currentIndex = timeRanges.indexOf(state.currentTimeRange);
+ if (currentIndex > 0) {
+ // 放大:使用更小的时间范围
+ state.currentTimeRange = timeRanges[currentIndex - 1];
+ }
+ }
+
+ // 更新显示
+ updateTimeRangeDisplay();
+ // 重新加载数据
+ loadMetrics();
+};
+
+// 为所有放大按钮添加事件监听器
+document.getElementById('cpuZoomInBtn')?.addEventListener('click', zoomInHandler);
+document.getElementById('memoryZoomInBtn')?.addEventListener('click', zoomInHandler);
+document.getElementById('diskZoomInBtn')?.addEventListener('click', zoomInHandler);
+document.getElementById('networkZoomInBtn')?.addEventListener('click', zoomInHandler);
+document.getElementById('speedZoomInBtn')?.addEventListener('click', zoomInHandler);
+
+// 缩小事件
+const zoomOutHandler = () => {
+ // 只在使用预设时间范围时生效
+ if (state.customStartTime && state.customEndTime) {
+ // 使用自定义时间时,先清除自定义时间
+ state.customStartTime = '';
+ state.customEndTime = '';
+ state.currentTimeRange = '1h';
+ } else {
+ // 查找当前时间范围在列表中的索引
+ const currentIndex = timeRanges.indexOf(state.currentTimeRange);
+ if (currentIndex < timeRanges.length - 1) {
+ // 缩小:使用更大的时间范围
+ state.currentTimeRange = timeRanges[currentIndex + 1];
+ }
+ }
+
+ // 更新显示
+ updateTimeRangeDisplay();
+ // 重新加载数据
+ loadMetrics();
+};
+
+// 为所有缩小按钮添加事件监听器
+document.getElementById('cpuZoomOutBtn')?.addEventListener('click', zoomOutHandler);
+document.getElementById('memoryZoomOutBtn')?.addEventListener('click', zoomOutHandler);
+document.getElementById('diskZoomOutBtn')?.addEventListener('click', zoomOutHandler);
+document.getElementById('networkZoomOutBtn')?.addEventListener('click', zoomOutHandler);
+document.getElementById('speedZoomOutBtn')?.addEventListener('click', zoomOutHandler);
+
+// 重置缩放处理函数
+const resetZoomHandler = () => {
+ // 重置时间范围
+ state.currentTimeRange = '1h';
+ state.customStartTime = '';
+ state.customEndTime = '';
+
+ // 更新显示
+ updateTimeRangeDisplay();
+ // 重新加载数据
+ loadMetrics();
+
+ // 重置所有图表的缩放
+ Object.values(charts).forEach(chart => {
+ if (chart && typeof chart.resetZoom === 'function') {
+ chart.resetZoom();
+ }
+ });
+};
+
+// 为所有重置按钮添加事件监听器
+document.getElementById('cpuResetZoomBtn')?.addEventListener('click', resetZoomHandler);
+document.getElementById('memoryResetZoomBtn')?.addEventListener('click', resetZoomHandler);
+document.getElementById('diskResetZoomBtn')?.addEventListener('click', resetZoomHandler);
+document.getElementById('networkResetZoomBtn')?.addEventListener('click', resetZoomHandler);
+document.getElementById('speedResetZoomBtn')?.addEventListener('click', resetZoomHandler);
// 工具函数
@@ -2656,6 +2899,35 @@ function initChartTabs() {
activeContainer.classList.remove('hidden');
}
+ // 显示/隐藏进程信息和磁盘详细信息
+ const processInfoContainer = document.getElementById('processInfoContainer');
+ const diskDetailsContainer = document.getElementById('diskDetailsContainer');
+
+ // 隐藏所有附加信息容器
+ if (processInfoContainer) {
+ processInfoContainer.classList.add('hidden');
+ }
+ if (diskDetailsContainer) {
+ diskDetailsContainer.classList.add('hidden');
+ }
+
+ // 根据选项卡显示相应的附加信息
+ if (tabId === 'cpu') {
+ // 显示进程信息
+ if (processInfoContainer) {
+ processInfoContainer.classList.remove('hidden');
+ // 加载进程信息
+ loadProcessInfo();
+ }
+ } else if (tabId === 'disk') {
+ // 显示磁盘详细信息
+ if (diskDetailsContainer) {
+ diskDetailsContainer.classList.remove('hidden');
+ // 加载磁盘详细信息
+ loadDiskDetails();
+ }
+ }
+
// 显示/隐藏网卡选择下拉框
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (interfaceContainer) {
@@ -2696,6 +2968,411 @@ window.addEventListener('DOMContentLoaded', () => {
handleHashChange();
});
+// 进程信息分页状态
+let processPagination = {
+ currentPage: 1,
+ itemsPerPage: 5,
+ totalItems: 0,
+ totalPages: 0,
+ allProcesses: []
+};
+
+// 加载进程信息
+async function loadProcessInfo(page = 1) {
+ const processTableBody = document.getElementById('processTableBody');
+ const processPaginationContainer = document.getElementById('processPaginationContainer');
+
+ if (!processTableBody) return;
+
+ try {
+ // 构建查询参数
+ const params = new URLSearchParams();
+ if (state.currentDeviceID) {
+ params.append('device_id', state.currentDeviceID);
+ }
+
+ // 如果是第一次加载,获取全部数据
+ if (processPagination.allProcesses.length === 0) {
+ // 发送请求
+ const response = await fetch(`${API_BASE_URL}/metrics/processes?${params.toString()}`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch process info');
+ }
+
+ const data = await response.json();
+ processPagination.allProcesses = data.data || [];
+ processPagination.totalItems = processPagination.allProcesses.length;
+ processPagination.totalPages = Math.ceil(processPagination.totalItems / processPagination.itemsPerPage);
+ }
+
+ // 更新当前页码
+ processPagination.currentPage = page;
+
+ // 清空表格
+ processTableBody.innerHTML = '';
+
+ if (processPagination.totalItems === 0) {
+ // 没有进程数据,显示提示
+ processTableBody.innerHTML = `
+
+
+ `;
+
+ // 隐藏分页控件
+ if (processPaginationContainer) {
+ processPaginationContainer.innerHTML = '';
+ }
+
+ return;
+ }
+
+ // 计算分页数据
+ const startIndex = (processPagination.currentPage - 1) * processPagination.itemsPerPage;
+ const endIndex = Math.min(startIndex + processPagination.itemsPerPage, processPagination.totalItems);
+ const paginatedProcesses = processPagination.allProcesses.slice(startIndex, endIndex);
+
+ // 填充表格数据
+ paginatedProcesses.forEach((proc, index) => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ 暂无进程数据
+
暂无磁盘详细信息
+加载磁盘详细信息失败
+