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.0

-

接收: 0 MB/s | 发送: 0 MB/s
总量: 接收 0 MB | 发送 0 MB

+

0.00 ↓

+

接收速率 0.00 MB | 发送速率 0.00 MB
接收总量 0.00 MB | 发送总量 0.00 MB

- +
+ @@ -289,12 +290,16 @@ + + + + +
+
+ 上次刷新: 刚刚 +
@@ -333,43 +344,161 @@
+ +
+
当前显示范围: 过去1小时
+
+ + + +
+
+
+ + +
+

进程信息

+
+ + + + + + + + + + + + + + + + +
进程名用户名进程IDCPU (%)内存 (%)路径命令行占用端口
+
+ +
+ + + - - -
-
当前显示范围: 过去24小时
-
- - - + +
+
当前显示范围: 过去1小时
+
+ + + +
+ + + + +
diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 8b731b0..224751c 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -6,16 +6,30 @@ let state = { currentTimeRange: '1h', // 与UI默认值保持一致 customStartTime: '', customEndTime: '', - currentInterval: '10m', // 固定10分钟区间 + currentInterval: '3m', // 固定10分钟区间 historyMetrics: {}, // 存储历史指标数据 - autoRefreshEnabled: false // 自动刷新开关状态,默认关闭 + autoRefreshEnabled: false, // 自动刷新开关状态,默认关闭 + lastMetricsUpdate: null, // 记录上次指标更新时间 + lastChartUpdate: 0, // 记录上次图表更新时间 + chartUpdateThrottle: 1000, // 图表更新节流时间(毫秒) + pendingMetricsUpdate: null, // 待处理的指标更新 + pendingChartUpdate: null, // 待处理的图表更新 + isUpdatingDOM: false, // 标记是否正在更新DOM + // 自动刷新相关配置 + autoRefreshInterval: 30000, // 默认自动刷新间隔(毫秒) + minAutoRefreshInterval: 5000, // 最小自动刷新间隔(毫秒) + maxAutoRefreshInterval: 60000, // 最大自动刷新间隔(毫秒) + autoRefreshTimer: null, // 自动刷新定时器 + lastAutoRefreshTime: 0 // 上次自动刷新时间 } // WebSocket连接 let ws = null; let wsReconnectAttempts = 0; -const wsMaxReconnectAttempts = 5; +const wsMaxReconnectAttempts = 10; // 增加最大重连次数 let wsReconnectDelay = 1000; +const wsMaxReconnectDelay = 30000; // 添加最大重连延迟(30秒) +let wsReconnectTimeout = null; // 重连定时器 // 图表实例 const charts = {}; @@ -29,14 +43,61 @@ function initApp() { initCharts(); initWebSocket(); - // 设置定时刷新 - setInterval(loadMetrics, 30000); + // 初始化自动刷新机制 + setupAutoRefresh(); + // 设置服务器数量定时刷新 setInterval(loadServerCount, 30000); // 初始化网卡列表 loadNetworkInterfaces(); } +// 设置自动刷新机制 +function setupAutoRefresh() { + // 清除现有的定时器 + if (state.autoRefreshTimer) { + clearInterval(state.autoRefreshTimer); + state.autoRefreshTimer = null; + } + + // 如果启用了自动刷新,设置新的定时器 + if (state.autoRefreshEnabled) { + // 立即执行一次,然后开始定时执行 + loadMetrics(); + // 设置定时器 + state.autoRefreshTimer = setInterval(() => { + loadMetrics(); + state.lastAutoRefreshTime = Date.now(); + // 动态调整刷新间隔 + updateAutoRefreshInterval(); + }, state.autoRefreshInterval); + } +} + +// 动态调整自动刷新间隔 +function updateAutoRefreshInterval() { + // 这里可以根据实际情况调整刷新间隔 + // 例如,根据系统负载、网络延迟或数据变化频率来动态调整 + // 简单实现:如果最近数据变化频繁,缩短刷新间隔;否则延长刷新间隔 + const now = Date.now(); + if (state.lastMetricsUpdate && (now - state.lastMetricsUpdate) < state.autoRefreshInterval / 2) { + // 数据变化频繁,缩短刷新间隔,但不低于最小值 + state.autoRefreshInterval = Math.max( + state.autoRefreshInterval - 2000, + state.minAutoRefreshInterval + ); + } else if (state.lastMetricsUpdate && (now - state.lastMetricsUpdate) > state.autoRefreshInterval * 2) { + // 数据变化缓慢,延长刷新间隔,但不高于最大值 + state.autoRefreshInterval = Math.min( + state.autoRefreshInterval + 5000, + state.maxAutoRefreshInterval + ); + } + + // 更新定时器 + setupAutoRefresh(); +} + // 初始化自定义时间范围 function initCustomTimeRange() { const now = new Date(); @@ -1105,14 +1166,19 @@ async function fetchMetric(metricType, aggregation = 'average') { // 构建查询参数 const params = new URLSearchParams(); + // 设置设备ID参数(如果存在) + if (state.currentDeviceID) { + params.append('device_id', state.currentDeviceID); + } + // 设置时间范围参数 if (state.customStartTime && state.customEndTime) { // 自定义时间范围 params.append('start_time', state.customStartTime); params.append('end_time', state.customEndTime); } else { - // 默认显示24小时的数据 - params.append('start_time', '-24h'); + // 使用状态中的时间范围设置 + params.append('start_time', `-${state.currentTimeRange}`); params.append('end_time', 'now()'); } @@ -1178,10 +1244,21 @@ async function loadMetrics() { // 更新图表 updateCharts(cpuData, memoryData, diskData, networkSumData); + + // 更新刷新状态指示器 + updateRefreshStatus(); } catch (error) { console.error('Failed to load metrics:', error); // 显示友好的错误提示 showToast('加载监控数据失败,请稍后重试', 'error'); + + // 更新刷新状态指示器为错误状态 + const statusIndicator = document.getElementById('refreshStatusIndicator'); + const lastRefreshTime = document.getElementById('lastRefreshTime'); + if (statusIndicator && lastRefreshTime) { + statusIndicator.className = 'w-2 h-2 bg-red-500 rounded-full animate-pulse'; + lastRefreshTime.textContent = `上次刷新: 失败`; + } } } @@ -1215,11 +1292,7 @@ function formatDiskDataForCards(diskData) { // 格式化网络数据,用于状态卡片显示 function formatNetworkDataForCards(networkData) { - // 如果networkData是空对象,返回空对象 - if (!networkData || typeof networkData !== 'object' || Array.isArray(networkData)) { - return {}; - } - + // 初始化返回数据结构 const formattedNetworkData = { bytes_sent: 0, bytes_received: 0, @@ -1227,22 +1300,107 @@ function formatNetworkDataForCards(networkData) { rx_bytes: 0 }; - // 遍历每个网卡 - for (const iface in networkData) { - const ifaceData = networkData[iface]; - - // 检查是否有sent和received数据 - if (ifaceData.sent && ifaceData.received) { - // 如果sent和received是数组,获取最新的数据点 - if (Array.isArray(ifaceData.sent) && ifaceData.sent.length > 0 && - Array.isArray(ifaceData.received) && ifaceData.received.length > 0) { - // 最新的数据点是数组的最后一个元素 - const latestSent = ifaceData.sent[ifaceData.sent.length - 1].value; - const latestReceived = ifaceData.received[ifaceData.received.length - 1].value; - - // 累加速率 - formattedNetworkData.bytes_sent += latestSent; - formattedNetworkData.bytes_received += latestReceived; + // 如果没有数据,返回初始值 + if (!networkData || typeof networkData === 'undefined') { + return formattedNetworkData; + } + + // 处理数组格式数据 + if (Array.isArray(networkData)) { + // 数组格式:直接处理最新的数据点 + if (networkData.length > 0) { + // 最新的数据点是数组的最后一个元素 + const latestData = networkData[networkData.length - 1]; + + // 检查是否包含速率数据 + if (latestData.sent !== undefined) { + formattedNetworkData.bytes_sent = latestData.sent; + } else if (latestData.bytes_sent !== undefined) { + formattedNetworkData.bytes_sent = latestData.bytes_sent; + } + + if (latestData.received !== undefined) { + formattedNetworkData.bytes_received = latestData.received; + } else if (latestData.bytes_received !== undefined) { + formattedNetworkData.bytes_received = latestData.bytes_received; + } + + // 检查是否包含总量数据 + if (latestData.tx_bytes !== undefined) { + formattedNetworkData.tx_bytes = latestData.tx_bytes; + } + if (latestData.rx_bytes !== undefined) { + formattedNetworkData.rx_bytes = latestData.rx_bytes; + } + } + } + // 处理对象格式数据 + else if (typeof networkData === 'object') { + // 检查是否为WebSocket直接返回的总流量格式 + if (networkData.bytes_sent !== undefined && networkData.bytes_received !== undefined) { + // WebSocket消息格式 - 总流量 + formattedNetworkData.bytes_sent = networkData.bytes_sent; + formattedNetworkData.bytes_received = networkData.bytes_received; + formattedNetworkData.tx_bytes = networkData.tx_bytes || 0; + formattedNetworkData.rx_bytes = networkData.rx_bytes || 0; + } + // 按网卡分组的数据 + else { + // 遍历每个网卡 + for (const iface in networkData) { + const ifaceData = networkData[iface]; + if (typeof ifaceData === 'object') { + // 处理速率数据 + if (ifaceData.sent && ifaceData.received) { + // 如果sent和received是数组,获取最新的数据点 + if (Array.isArray(ifaceData.sent) && ifaceData.sent.length > 0 && + Array.isArray(ifaceData.received) && ifaceData.received.length > 0) { + // 最新的数据点是数组的最后一个元素 + const latestSent = ifaceData.sent[ifaceData.sent.length - 1].value; + const latestReceived = ifaceData.received[ifaceData.received.length - 1].value; + + // 累加速率 + formattedNetworkData.bytes_sent += latestSent; + formattedNetworkData.bytes_received += latestReceived; + } + // 如果sent和received是数值 + else if (typeof ifaceData.sent === 'number' && typeof ifaceData.received === 'number') { + formattedNetworkData.bytes_sent += ifaceData.sent; + formattedNetworkData.bytes_received += ifaceData.received; + } + } + // 直接使用bytes_sent和bytes_received字段 + else if (ifaceData.bytes_sent !== undefined && ifaceData.bytes_received !== undefined) { + formattedNetworkData.bytes_sent += ifaceData.bytes_sent; + formattedNetworkData.bytes_received += ifaceData.bytes_received; + } + + // 检查是否有总量数据(多种可能的字段名) + // 1. 标准字段名:tx_bytes, rx_bytes + if (ifaceData.tx_bytes !== undefined) { + formattedNetworkData.tx_bytes += ifaceData.tx_bytes; + } + if (ifaceData.rx_bytes !== undefined) { + formattedNetworkData.rx_bytes += ifaceData.rx_bytes; + } + + // 2. 旧格式字段名:bytes_sent_total, bytes_received_total + if (ifaceData.bytes_sent_total !== undefined) { + formattedNetworkData.tx_bytes += ifaceData.bytes_sent_total; + } + if (ifaceData.bytes_received_total !== undefined) { + formattedNetworkData.rx_bytes += ifaceData.bytes_received_total; + } + + // 3. 可能的其他格式:bytes_sent, bytes_received作为总量数据 + // 仅当没有其他总量数据字段时使用 + if (formattedNetworkData.tx_bytes === 0 && ifaceData.bytes_sent !== undefined && typeof ifaceData.bytes_sent === 'number') { + formattedNetworkData.tx_bytes += ifaceData.bytes_sent; + } + if (formattedNetworkData.rx_bytes === 0 && ifaceData.bytes_received !== undefined && typeof ifaceData.bytes_received === 'number') { + formattedNetworkData.rx_bytes += ifaceData.bytes_received; + } + } } } } @@ -1317,12 +1475,44 @@ async function loadServerInfo(deviceId) { // 处理指标更新 function handleMetricsUpdate(message) { const { device_id, metrics } = message; + // 格式化数据,确保updateStatusCards函数能正确处理 + const formattedMetrics = { + cpu: metrics.cpu, + cpu_hz: metrics.cpu_hz, // 添加cpu_hz字段,确保CPU频率能显示 + memory: metrics.memory, + disk: formatDiskDataForCards(metrics.disk), + network: formatNetworkDataForCards(metrics.network) + }; // 直接更新统计卡片,始终实时更新 - updateStatusCards(metrics); - // 根据自动刷新开关状态决定是否刷新图表 + updateStatusCards(formattedMetrics); + // 根据自动刷新开关状态决定是否更新图表 if (state.autoRefreshEnabled) { - // 立即刷新数据以确保图表也更新 - setTimeout(() => loadMetrics(), 500); + // 直接使用WebSocket数据更新图表,避免不必要的API请求 + updateCharts(metrics.cpu, metrics.memory, metrics.disk, metrics.network); + } +} + +// 更新刷新状态指示器 +function updateRefreshStatus() { + // 更新最后刷新时间 + const now = new Date(); + const formattedTime = now.toLocaleTimeString(); + + // 更新状态指示器和时间显示 + const statusIndicator = document.getElementById('refreshStatusIndicator'); + const lastRefreshTime = document.getElementById('lastRefreshTime'); + + if (statusIndicator && lastRefreshTime) { + // 短暂显示绿色,表示数据已更新 + statusIndicator.className = 'w-2 h-2 bg-green-500 rounded-full'; + lastRefreshTime.textContent = `上次刷新: ${formattedTime}`; + + // 1秒后恢复为黄色,表示正常状态 + setTimeout(() => { + if (statusIndicator) { + statusIndicator.className = 'w-2 h-2 bg-yellow-500 rounded-full'; + } + }, 1000); } } @@ -1331,6 +1521,22 @@ function updateStatusCards(metrics) { // 更新历史指标数据 updateHistoryMetrics(metrics); + // 保存待处理的指标更新 + state.pendingMetricsUpdate = metrics; + + // 使用requestAnimationFrame优化DOM更新 + if (!state.isUpdatingDOM) { + state.isUpdatingDOM = true; + requestAnimationFrame(() => { + _updateStatusCards(state.pendingMetricsUpdate); + state.isUpdatingDOM = false; + state.lastMetricsUpdate = Date.now(); + }); + } +} + +// 内部DOM更新函数,由requestAnimationFrame调用 +function _updateStatusCards(metrics) { // 使用历史数据或当前数据 const displayMetrics = { cpu: metrics.cpu || state.historyMetrics.cpu, @@ -1544,128 +1750,87 @@ function updateStatusCards(metrics) { } } - // 更新网络状态卡片 + // 更新网络流量状态卡片 if (displayMetrics.network) { - let sentRate = 0; - let receivedRate = 0; - let sentTotal = 0; - let receivedTotal = 0; - // 解析网络数据 - if (typeof displayMetrics.network === 'object') { - if (Array.isArray(displayMetrics.network)) { - // 数组格式的数据 - if (displayMetrics.network.length > 0) { - // 获取最新的数据点 - const latestData = displayMetrics.network[displayMetrics.network.length - 1]; - sentRate = latestData.sent || 0; - receivedRate = latestData.received || 0; - sentTotal = latestData.tx_bytes || 0; - receivedTotal = latestData.rx_bytes || 0; - } - } else if (displayMetrics.network.sent && displayMetrics.network.received) { - // 包含sent和received字段的数据 - if (Array.isArray(displayMetrics.network.sent) && displayMetrics.network.sent.length > 0 && - Array.isArray(displayMetrics.network.received) && displayMetrics.network.received.length > 0) { - // 处理数组格式的速率数据 - sentRate = displayMetrics.network.sent[displayMetrics.network.sent.length - 1].value; - receivedRate = displayMetrics.network.received[displayMetrics.network.received.length - 1].value; - } else if (typeof displayMetrics.network.sent === 'number' && typeof displayMetrics.network.received === 'number') { - // 处理数值格式的速率数据 - sentRate = displayMetrics.network.sent; - receivedRate = displayMetrics.network.received; - } - - // 处理总量数据 - sentTotal = displayMetrics.network.tx_bytes || displayMetrics.network.sent_total || 0; - receivedTotal = displayMetrics.network.rx_bytes || displayMetrics.network.received_total || 0; - } else if (displayMetrics.network.bytes_sent !== undefined && displayMetrics.network.bytes_received !== undefined) { - // WebSocket消息格式 - 总流量 - sentRate = displayMetrics.network.bytes_sent; - receivedRate = displayMetrics.network.bytes_received; - - // 优先使用tx_bytes和rx_bytes作为总量 - sentTotal = displayMetrics.network.tx_bytes || 0; - receivedTotal = displayMetrics.network.rx_bytes || 0; + const rxRate = displayMetrics.network.bytes_received || 0; // 接收速率 (bytes/s) + const txRate = displayMetrics.network.bytes_sent || 0; // 发送速率 (bytes/s) + + // 计算速率比值 + let ratio = 0; + let ratioSymbol = ''; + let ratioText = ''; + let symbolColor = ''; + + if (rxRate === 0 && txRate === 0) { + // 如果接收速率和发送速率都为0,显示无穷符号 + ratioText = '∞'; + ratioSymbol = ''; + symbolColor = 'text-gray-500'; + } else if (txRate === 0) { + // 如果发送速率为0,显示无穷符号和接收箭头 + ratioText = '∞'; + ratioSymbol = '↓'; + symbolColor = 'text-green-500'; + } else if (rxRate === 0) { + // 如果接收速率为0,显示无穷符号和发送箭头 + ratioText = '∞'; + ratioSymbol = '↑'; + symbolColor = 'text-red-500'; + } else { + // 计算接收速率与发送速率的比值 + ratio = rxRate / txRate; + ratioText = ratio.toFixed(2); + + // 根据比值判断箭头方向和颜色 + if (ratio > 10) { + // 接收速率远高于发送,显示绿色↓ + ratioSymbol = '↓'; + symbolColor = 'text-green-500'; + } else if (ratio < 0.1) { + // 发送速率远高于接收,显示红色↑ + ratioSymbol = '↑'; + symbolColor = 'text-red-500'; + } else if (ratio >= 0.5 && ratio <= 2) { + // 收发速率均衡,显示蓝色↔ + ratioSymbol = '↔'; + symbolColor = 'text-blue-500'; + } else if (ratio > 1) { + // 接收速率高于发送,显示绿色↓ + ratioSymbol = '↓'; + symbolColor = 'text-green-500'; } else { - // 按网卡分组的数据,计算总流量 - for (const iface in displayMetrics.network) { - const ifaceMetrics = displayMetrics.network[iface]; - if (typeof ifaceMetrics === 'object') { - // 计算速率 - if (ifaceMetrics.bytes_sent !== undefined) { - sentRate += ifaceMetrics.bytes_sent; - } else if (ifaceMetrics.sent !== undefined) { - sentRate += ifaceMetrics.sent; - } - - if (ifaceMetrics.bytes_received !== undefined) { - receivedRate += ifaceMetrics.bytes_received; - } else if (ifaceMetrics.received !== undefined) { - receivedRate += ifaceMetrics.received; - } - - // 计算总量(所有网卡的tx_bytes和rx_bytes之和) - if (ifaceMetrics.tx_bytes !== undefined) { - sentTotal += ifaceMetrics.tx_bytes; - } else if (ifaceMetrics.bytes_sent_total !== undefined) { - sentTotal += ifaceMetrics.bytes_sent_total; - } - - if (ifaceMetrics.rx_bytes !== undefined) { - receivedTotal += ifaceMetrics.rx_bytes; - } else if (ifaceMetrics.bytes_received_total !== undefined) { - receivedTotal += ifaceMetrics.bytes_received_total; - } - } - } + // 发送速率高于接收,显示红色↑ + ratioSymbol = '↑'; + symbolColor = 'text-red-500'; } } - // 如果总量为0,尝试使用速率作为总量(临时解决方案) - if (sentTotal === 0) { - sentTotal = sentRate; - } - if (receivedTotal === 0) { - receivedTotal = receivedRate; - } + // 格式化速率为MB/s + const formatRate = (bytesPerSec) => { + return (bytesPerSec / (1024 * 1024)).toFixed(2); + }; - // 计算比率并格式化显示 - let ratioDisplay = "0.0"; - let ratioDirection = " / "; - - if (sentRate === 0 && receivedRate === 0) { - // 没有流量 - ratioDisplay = "0.0"; - } else if (sentRate === 0) { - // 只有接收流量 - ratioDisplay = "∞"; - ratioDirection = ""; - } else if (receivedRate === 0) { - // 只有发送流量 - ratioDisplay = "0.0"; - ratioDirection = ""; - } else { - // 正常计算比率 - const ratio = receivedRate / sentRate; - ratioDisplay = ratio.toFixed(1); - } + const rxRateMB = formatRate(rxRate); + const txRateMB = formatRate(txRate); // 更新显示 - const networkValueElement = document.getElementById('networkValue'); + const networkElement = document.getElementById('networkValue'); const networkDetailsElement = document.getElementById('networkDetails'); - if (networkValueElement) { - networkValueElement.className = 'text-3xl font-bold text-gray-900 metric-value'; - networkValueElement.innerHTML = `${ratioDisplay} ${ratioDirection}`; + + if (networkElement) { + // 大数字显示比值和箭头 + networkElement.innerHTML = `${ratioText} ${ratioSymbol}`; + networkElement.className = 'text-3xl font-bold metric-value text-gray-900 flex items-center'; } + if (networkDetailsElement) { + // 速率显示 networkDetailsElement.className = 'text-xs text-gray-500 mt-1'; - networkDetailsElement.innerHTML = - `接收: ${formatBytes(receivedRate, 2, true)} | ` + - `发送: ${formatBytes(sentRate, 2, true)}
` + - `总量: 接收 ${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 = ` + ${proc.process_name || 'N/A'} + ${proc.username || 'N/A'} + ${proc.pid || 'N/A'} + ${proc.cpu_usage ? parseFloat(proc.cpu_usage).toFixed(2) : '0.00'} + ${proc.memory_usage ? parseFloat(proc.memory_usage).toFixed(2) : '0.00'} + ${proc.path || 'N/A'} + ${proc.cmdline || 'N/A'} + ${proc.ports || 'N/A'} + `; + processTableBody.appendChild(row); + }); + + // 创建分页控件 + if (processPaginationContainer) { + createPaginationControls(processPaginationContainer, processPagination, loadProcessInfo); + } + } catch (error) { + console.error('Error loading process info:', error); + processTableBody.innerHTML = ` + + 加载进程信息失败 + + `; + + if (processPaginationContainer) { + processPaginationContainer.innerHTML = ''; + } + } +} + +// 磁盘信息分页状态 +let diskPagination = { + currentPage: 1, + itemsPerPage: 5, + totalItems: 0, + totalPages: 0, + allDisks: [] +}; + +// 创建分页控件 +function createPaginationControls(container, paginationState, loadFunction) { + // 清空容器 + container.innerHTML = ''; + + // 如果只有一页,不需要分页 + if (paginationState.totalPages <= 1) { + return; + } + + const pagination = document.createElement('div'); + pagination.className = 'flex justify-between items-center mt-4'; + + // 页码信息 + const info = document.createElement('div'); + info.className = 'text-sm text-gray-500'; + info.textContent = `显示 ${(paginationState.currentPage - 1) * paginationState.itemsPerPage + 1} 至 ${Math.min(paginationState.currentPage * paginationState.itemsPerPage, paginationState.totalItems)} 条,共 ${paginationState.totalItems} 条`; + + // 分页按钮容器 + const buttons = document.createElement('div'); + buttons.className = 'flex items-center space-x-2'; + + // 上一页按钮 + const prevButton = document.createElement('button'); + prevButton.className = `px-3 py-1 rounded border ${paginationState.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50'}`; + prevButton.innerHTML = ''; + prevButton.disabled = paginationState.currentPage === 1; + prevButton.addEventListener('click', () => { + if (paginationState.currentPage > 1) { + loadFunction(paginationState.currentPage - 1); + } + }); + + // 页码按钮 + const pageButtons = []; + + // 总是显示第一页 + if (paginationState.currentPage > 3) { + pageButtons.push(1); + pageButtons.push('...'); + } + + // 显示当前页附近的页码 + const startPage = Math.max(1, paginationState.currentPage - 2); + const endPage = Math.min(paginationState.totalPages, paginationState.currentPage + 2); + + for (let i = startPage; i <= endPage; i++) { + pageButtons.push(i); + } + + // 总是显示最后一页 + if (paginationState.currentPage < paginationState.totalPages - 2) { + pageButtons.push('...'); + pageButtons.push(paginationState.totalPages); + } + + // 创建页码按钮 + pageButtons.forEach(page => { + if (page === '...') { + const ellipsis = document.createElement('span'); + ellipsis.className = 'px-2 text-gray-500'; + ellipsis.textContent = '...'; + buttons.appendChild(ellipsis); + } else { + const pageButton = document.createElement('button'); + pageButton.className = `px-3 py-1 rounded border ${page === paginationState.currentPage ? 'bg-blue-50 text-blue-600 border-blue-300' : 'bg-white text-gray-700 hover:bg-gray-50'}`; + pageButton.textContent = page; + pageButton.addEventListener('click', () => { + loadFunction(page); + }); + buttons.appendChild(pageButton); + } + }); + + // 下一页按钮 + const nextButton = document.createElement('button'); + nextButton.className = `px-3 py-1 rounded border ${paginationState.currentPage === paginationState.totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50'}`; + nextButton.innerHTML = ''; + nextButton.disabled = paginationState.currentPage === paginationState.totalPages; + nextButton.addEventListener('click', () => { + if (paginationState.currentPage < paginationState.totalPages) { + loadFunction(paginationState.currentPage + 1); + } + }); + + // 添加到按钮容器 + buttons.appendChild(prevButton); + + // 跳转页面功能 + const jumpContainer = document.createElement('div'); + jumpContainer.className = 'flex items-center ml-4 space-x-2'; + + const jumpText = document.createElement('span'); + jumpText.className = 'text-sm text-gray-500'; + jumpText.textContent = '前往'; + + const jumpInput = document.createElement('input'); + jumpInput.type = 'number'; + jumpInput.min = 1; + jumpInput.max = paginationState.totalPages; + jumpInput.value = paginationState.currentPage; + jumpInput.className = 'w-12 px-2 py-1 text-sm border rounded text-center'; + jumpInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + let page = parseInt(jumpInput.value); + if (!isNaN(page) && page >= 1 && page <= paginationState.totalPages) { + loadFunction(page); + } else { + jumpInput.value = paginationState.currentPage; + } + } + }); + + const totalText = document.createElement('span'); + totalText.className = 'text-sm text-gray-500'; + totalText.textContent = `页,共 ${paginationState.totalPages} 页`; + + jumpContainer.appendChild(jumpText); + jumpContainer.appendChild(jumpInput); + jumpContainer.appendChild(totalText); + + buttons.appendChild(jumpContainer); + buttons.appendChild(nextButton); + + // 添加到分页容器 + pagination.appendChild(info); + pagination.appendChild(buttons); + + // 添加到页面 + container.appendChild(pagination); +} + +// 加载磁盘详细信息 +async function loadDiskDetails(page = 1) { + const diskDetailsContent = document.getElementById('diskDetailsContent'); + const diskPaginationContainer = document.getElementById('diskPaginationContainer'); + + if (!diskDetailsContent) return; + + try { + // 构建查询参数 + const params = new URLSearchParams(); + if (state.currentDeviceID) { + params.append('device_id', state.currentDeviceID); + } + + // 如果是第一次加载,获取全部数据 + if (diskPagination.allDisks.length === 0) { + // 发送请求 + const response = await fetch(`${API_BASE_URL}/metrics/disk_details?${params.toString()}`); + if (!response.ok) { + throw new Error('Failed to fetch disk details'); + } + + const data = await response.json(); + diskPagination.allDisks = data.data || []; + diskPagination.totalItems = diskPagination.allDisks.length; + diskPagination.totalPages = Math.ceil(diskPagination.totalItems / diskPagination.itemsPerPage); + } + + // 更新当前页码 + diskPagination.currentPage = page; + + // 清空内容 + diskDetailsContent.innerHTML = ''; + + if (diskPagination.totalItems === 0) { + // 没有磁盘数据,显示提示 + diskDetailsContent.innerHTML = ` +
+ +

暂无磁盘详细信息

+
+ `; + + // 隐藏分页控件 + if (diskPaginationContainer) { + diskPaginationContainer.innerHTML = ''; + } + + return; + } + + // 计算分页数据 + const startIndex = (diskPagination.currentPage - 1) * diskPagination.itemsPerPage; + const endIndex = Math.min(startIndex + diskPagination.itemsPerPage, diskPagination.totalItems); + const paginatedDisks = diskPagination.allDisks.slice(startIndex, endIndex); + + // 填充磁盘详细信息卡片 + paginatedDisks.forEach(disk => { + const diskCard = document.createElement('div'); + diskCard.className = 'bg-white rounded-lg shadow-md p-6 border border-gray-100'; + diskCard.innerHTML = ` +

${disk.device_id || 'Unknown Device'}

+
+
+ 状态: + ${disk.status || 'Unknown'} +
+
+ 类型: + ${disk.type || 'Unknown'} +
+
+ 大小: + ${disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00'} GB +
+
+ 型号: + ${disk.model || 'Unknown'} +
+
+ 接口类型: + ${disk.interface_type || 'Unknown'} +
+
+ 描述: + ${disk.description || 'Unknown'} +
+
+ `; + diskDetailsContent.appendChild(diskCard); + }); + + // 创建分页控件 + if (diskPaginationContainer) { + createPaginationControls(diskPaginationContainer, diskPagination, loadDiskDetails); + } + } catch (error) { + console.error('Error loading disk details:', error); + diskDetailsContent.innerHTML = ` +
+ +

加载磁盘详细信息失败

+
+ `; + + if (diskPaginationContainer) { + diskPaginationContainer.innerHTML = ''; + } + } +} + +// 加载系统日志 +async function loadSystemLogs() { + const logTableBody = document.getElementById('logTableBody'); + if (!logTableBody) return; + + try { + // 构建查询参数 + const params = new URLSearchParams(); + if (state.currentDeviceID) { + params.append('device_id', state.currentDeviceID); + } + + // 发送请求 + const response = await fetch(`${API_BASE_URL}/metrics/logs?${params.toString()}`); + if (!response.ok) { + throw new Error('Failed to fetch system logs'); + } + + const data = await response.json(); + const logs = data.data; + + // 清空表格 + logTableBody.innerHTML = ''; + + if (logs.length === 0) { + // 没有日志数据,显示提示 + logTableBody.innerHTML = ` + + 暂无系统日志 + + `; + return; + } + + // 填充表格数据 + logs.forEach((log, index) => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${index + 1} + ${log.source || 'System'} + ${log.time || new Date().toLocaleString()} + ${log.message || 'No message'} + `; + logTableBody.appendChild(row); + }); + } catch (error) { + console.error('Error loading system logs:', error); + logTableBody.innerHTML = ` + + 加载系统日志失败 + + `; + } +} + // 处理hash变化 function handleHashChange() { const hash = window.location.hash; @@ -2703,6 +3380,11 @@ function handleHashChange() { // 延迟一下,确保DOM已经渲染完成 setTimeout(() => { loadMetrics(); + // 加载进程信息(如果当前是CPU选项卡) + const activeTab = document.querySelector('.chart-tab.active'); + if (activeTab && activeTab.dataset.tab === 'cpu') { + loadProcessInfo(); + } }, 300); } } diff --git a/backend/static/network_data_analysis.md b/backend/static/network_data_analysis.md new file mode 100644 index 0000000..850a324 --- /dev/null +++ b/backend/static/network_data_analysis.md @@ -0,0 +1,98 @@ +# 网络流量数据来源分析 + +## 数据来源 + +### 1. API调用 +- **函数**:`loadMetrics()` +- **调用方式**:`fetchMetric('network')` +- **API路径**:`${API_BASE_URL}/metrics/network` +- **参数**: + - `start_time`: 起始时间 + - `end_time`: 结束时间 + - `aggregation`: 聚合方式(默认为average) + - `interval`: 时间区间(固定为10m) + +### 2. WebSocket实时更新 +- **函数**:`handleMetricsUpdate(message)` +- **消息类型**:`metrics_update` +- **数据结构**: + ```json + { + "type": "metrics_update", + "device_id": "device_id", + "metrics": { + "network": {...} + } + } + ``` + +## 数据处理流程 + +### 1. 数据格式化 +- **函数**:`formatNetworkDataForCards(networkData)` +- **作用**:将API或WebSocket返回的原始网络数据格式化为状态卡片可用的格式 +- **处理逻辑**: + - 初始化返回数据结构,包含发送速率、接收速率、发送总量、接收总量 + - 根据数据类型(数组或对象)进行不同处理 + - **遍历所有网卡**,累加所有网卡的流量数据 + - 支持多种数据格式,包括数组格式、WebSocket消息格式、按网卡分组的数据格式 + - 支持旧格式的总量数据(bytes_sent_total, bytes_received_total) + +### 2. 数据更新 +- **函数**:`_updateStatusCards(metrics)` +- **作用**:更新状态卡片的显示内容 +- **网络流量卡片更新逻辑**: + - 解析网络数据,获取发送速率、接收速率、发送总量、接收总量 + - 计算接收速率/发送速率的比值,根据比值显示箭头 + - 使用`formatBytes`函数格式化速率和总量,自动处理MB和GB的转换 + - 更新DOM元素,显示比值、速率和总量 + +## 显示内容 + +### 1. 大数字显示 +- **内容**:接收速率/发送速率的比值 +- **逻辑**: + - 如果接收速率和发送速率都为0,显示无穷符号(∞) + - 如果发送速率为0,接收速率不为0,显示无穷符号(∞)和↓ + - 如果接收速率为0,发送速率不为0,显示0和↑ + - 正常情况:计算比值,显示比值和箭头(比值>1显示↓,否则显示↑) + +### 2. 速率显示 +- **格式**:接收速率 MB/s | 发送速率 MB/s +- **处理**:使用`formatBytes`函数格式化,自动处理MB/s和GB/s的转换 + +### 3. 总量显示 +- **格式**:接收总量 MB | 发送总量 MB +- **处理**:使用`formatBytes`函数格式化,自动处理MB和GB的转换 + +## 正确性验证 + +### 1. 网卡总流量 +- `formatNetworkDataForCards`函数会遍历所有网卡,累加所有网卡的流量数据 +- 这样显示的是所有网卡的总流量信息,符合用户需求 + +### 2. 数据类型转换 +- `formatBytes`函数会根据数据大小自动转换为合适的单位(MB或GB) +- 速率显示带有正确的单位(MB/s或GB/s) +- 总量显示带有正确的单位(MB或GB) + +### 3. 边界情况处理 +- 处理了接收速率和发送速率都为0的情况 +- 处理了发送速率为0,接收速率不为0的情况 +- 处理了接收速率为0,发送速率不为0的情况 + +## 代码优化 + +### 1. 已完成的优化 +- 使用`formatBytes`函数格式化流量数据,自动处理MB和GB的转换 +- 确保显示的是所有网卡的总流量信息 +- 处理了各种边界情况 + +### 2. 优化效果 +- 显示内容更准确,带有正确的单位 +- 自动适应数据大小,提高可读性 +- 处理了各种边界情况,避免显示错误 + +## 结论 + +网络流量卡片的数据来源是可靠的,通过API调用或WebSocket实时更新获取原始数据,经过`formatNetworkDataForCards`函数格式化处理后,显示的是所有网卡的总流量信息。使用`formatBytes`函数格式化后,显示内容带有正确的单位,并且会根据数据大小自动转换为合适的单位(MB或GB),提高了数据的可读性。 \ No newline at end of file diff --git a/test_agent.json b/test_agent.json deleted file mode 100644 index fe4a7e6..0000000 --- a/test_agent.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "server_url": "http://localhost:8080/api", - "id": "test-agent", - "name": "Test Agent", - "device_id": "test-device", - "token": "bb30bfaee01bf7b541bbefe422f72645", - "interval": "5s", - "debug": true, - "api_port": 8081 -} \ No newline at end of file diff --git a/test_metrics.sh b/test_metrics.sh deleted file mode 100755 index 9bec916..0000000 --- a/test_metrics.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# 测试发送单个指标对象 -echo "测试发送单个指标对象:" -curl -v -X POST -H "Content-Type: application/json" -H "X-Device-ID: test-device" -H "X-Agent-Name: test-agent" -H "X-Device-Token: test-token" -d '{"cpu": 50.5, "memory": 30.2, "disk": {":/": 45.6}, "network": {"eth0": {"bytes_sent": 1000, "bytes_received": 2000}}}' http://localhost:8080/api/metrics/ - -echo "\n---\n" - -# 测试发送指标数组 -echo "测试发送指标数组:" -curl -v -X POST -H "Content-Type: application/json" -H "X-Device-ID: test-device" -H "X-Agent-Name: test-agent" -H "X-Device-Token: test-token" -d '[{"cpu": 50.5, "memory": 30.2, "disk": {":/": 45.6}, "network": {"eth0": {"bytes_sent": 1000, "bytes_received": 2000}}}, {"cpu": 60.5, "memory": 40.2, "disk": {":/": 55.6}, "network": {"eth0": {"bytes_sent": 2000, "bytes_received": 3000}}}]' http://localhost:8080/api/metrics/