//go:build linux package collector import ( "bufio" "context" "encoding/hex" "errors" "fmt" "io/fs" "net" "os" "os/user" "path/filepath" "runtime" "sort" "strconv" "strings" "syscall" "time" "mfd-monitor-agent/internal/status" ) var socketInodesFunc = socketInodes var readTCPListenersFunc = readTCPListeners // collectSystem 从 Linux 填充主机标识、操作系统、架构与运行时长字段。 func collectSystem(_ context.Context, hostID string, out *status.HostStatus) error { hostname, err := os.Hostname() if err != nil { return err } out.Host.HostID = hostID out.Host.Hostname = hostname out.Host.Platform = runtime.GOOS out.Host.PlatformVersion = readOSRelease() out.Host.Arch = runtime.GOARCH out.Host.UptimeSeconds = readUptime() if out.Host.HostID == "" { out.Host.HostID = hostname } return nil } // collectNetwork 填充 Linux 网卡、地址与 MAC 数据。 func collectNetwork(_ context.Context, out *status.HostStatus) error { ifaces, err := net.Interfaces() if err != nil { return err } ipSet := map[string]struct{}{} macSet := map[string]struct{}{} for _, iface := range ifaces { addrs, _ := iface.Addrs() item := status.NetworkInterface{ Name: iface.Name, MAC: iface.HardwareAddr.String(), MTU: iface.MTU, Flags: strings.Fields(iface.Flags.String()), } for _, addr := range addrs { value := addr.String() item.Addresses = append(item.Addresses, value) ipSet[value] = struct{}{} } if item.MAC != "" { macSet[item.MAC] = struct{}{} } out.Network.Interfaces = append(out.Network.Interfaces, item) } out.Network.IPv4s, out.Network.IPv6s = networkAddressSets(ipSet) out.Network.MACs = sortedKeys(macSet) return nil } // collectResource 从 Linux /proc 文件填充 CPU、内存与磁盘资源数据。 func collectResource(ctx context.Context, out *status.HostStatus) error { out.CPU.LogicalCores = runtime.NumCPU() out.CPU.PhysicalCores, out.CPU.ModelName = readCPUInfo() out.CPU.LoadAvg = readLoadAvg() out.CPU.UsagePercent = readCPUUsagePercent(ctx) out.Memory = readMemory() disks, err := readDisks() if err != nil { return err } out.Disks = disks return nil } // collectProcesses 读取 /proc 进程目录;部分条目失败时降级为 partial。 func collectProcesses(_ context.Context, out *status.HostStatus) error { entries, err := os.ReadDir("/proc") if err != nil { out.Processes.Status = "error" return err } var errs []error for _, entry := range entries { if !entry.IsDir() { continue } pid, err := strconv.Atoi(entry.Name()) if err != nil { continue } process, err := readProcess(pid) if err != nil { errs = append(errs, err) continue } out.Processes.Items = append(out.Processes.Items, process) } sort.Slice(out.Processes.Items, func(i, j int) bool { return out.Processes.Items[i].PID < out.Processes.Items[j].PID }) if len(errs) > 0 { out.Processes.Status = "partial" return fmt.Errorf("%d process entries could not be read", len(errs)) } out.Processes.Status = "ok" return nil } // collectSockets 读取 Linux TCP 监听表,并将 socket inode 映射到所属进程。 func collectSockets(_ context.Context, out *status.HostStatus) error { inodes, inodeErr := socketInodesFunc() var listeners []status.TCPListenerInfo var errs []error if inodeErr != nil { errs = append(errs, inodeErr) } for _, file := range []struct { path string family string }{ {path: "/proc/net/tcp", family: "ipv4"}, {path: "/proc/net/tcp6", family: "ipv6"}, } { items, err := readTCPListenersFunc(file.path, file.family, inodes) if err != nil { errs = append(errs, err) continue } listeners = append(listeners, items...) } sort.Slice(listeners, func(i, j int) bool { if listeners[i].IP == listeners[j].IP { return listeners[i].Port < listeners[j].Port } return listeners[i].IP < listeners[j].IP }) out.TCPListeners.Items = listeners if len(errs) > 0 { out.TCPListeners.Status = "partial" return errors.Join(errs...) } out.TCPListeners.Status = "ok" return nil } // readOSRelease 在可读时从 /etc/os-release 返回 PRETTY_NAME。 func readOSRelease() string { file, err := os.Open("/etc/os-release") if err != nil { return "" } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "PRETTY_NAME=") { return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), `"`) } } return "" } // readUptime 从 /proc/uptime 返回主机运行时长(秒)。 func readUptime() uint64 { data, err := os.ReadFile("/proc/uptime") if err != nil { return 0 } fields := strings.Fields(string(data)) if len(fields) == 0 { return 0 } value, _ := strconv.ParseFloat(fields[0], 64) return uint64(value) } // readCPUInfo 从 /proc/cpuinfo 返回物理 CPU 封装数与首个型号名称。 func readCPUInfo() (int, string) { file, err := os.Open("/proc/cpuinfo") if err != nil { return 0, "" } defer file.Close() physical := map[string]struct{}{} model := "" scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "physical id": physical[value] = struct{}{} case "model name": if model == "" { model = value } } } return len(physical), model } // readLoadAvg 在可用时返回 Linux 1/5/15 分钟负载均值。 func readLoadAvg() []float64 { data, err := os.ReadFile("/proc/loadavg") if err != nil { return nil } fields := strings.Fields(string(data)) var values []float64 for i := 0; i < 3 && i < len(fields); i++ { value, err := strconv.ParseFloat(fields[i], 64) if err == nil { values = append(values, value) } } return values } // readMemory 从 /proc/meminfo 返回以字节为单位的 Linux 内存计数。 func readMemory() status.MemoryInfo { values := map[string]uint64{} file, err := os.Open("/proc/meminfo") if err != nil { return status.MemoryInfo{} } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) < 2 { continue } value, _ := strconv.ParseUint(fields[1], 10, 64) // /proc/meminfo 以 KiB 报告内存;API 对外暴露字节, // 并通过 memory.unit 让客户端可明确展示单位。 values[strings.TrimSuffix(fields[0], ":")] = value * 1024 } total := values["MemTotal"] free := values["MemFree"] available := values["MemAvailable"] used := total - available percent := 0.0 if total > 0 { percent = float64(used) / float64(total) * 100 } return status.MemoryInfo{Unit: "byte", Total: total, Used: used, Free: free, Available: available, UsedPercent: percent} } // readDisks 从 /proc/mounts 与 statfs 返回物理盘与网络挂载文件系统用量。 func readDisks() ([]status.DiskInfo, error) { file, err := os.Open("/proc/mounts") if err != nil { return nil, err } defer file.Close() var disks []status.DiskInfo scanner := bufio.NewScanner(file) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) < 3 { continue } var stat syscall.Statfs_t if err := syscall.Statfs(fields[1], &stat); err != nil { continue } total := stat.Blocks * uint64(stat.Bsize) if !shouldIncludeDiskMount(fields[0], fields[2], total) { continue } free := stat.Bavail * uint64(stat.Bsize) used := total - free percent := 0.0 if total > 0 { percent = float64(used) / float64(total) * 100 } disks = append(disks, status.DiskInfo{ Device: fields[0], MountPoint: fields[1], FSType: fields[2], Unit: "byte", Total: total, Used: used, Free: free, UsedPercent: percent, }) } return disks, scanner.Err() } // shouldIncludeDiskMount 判断挂载点是否代表用户关心的磁盘。 func shouldIncludeDiskMount(device string, fsType string, total uint64) bool { if total == 0 { return false } networkFSTypes := map[string]bool{ "nfs": true, "nfs4": true, "cifs": true, "smb3": true, "sshfs": true, } if networkFSTypes[fsType] { return true } return strings.HasPrefix(device, "/dev/") } // readProcess 从 /proc/ 返回一条 Linux 进程记录。 func readProcess(pid int) (status.ProcessInfo, error) { base := filepath.Join("/proc", strconv.Itoa(pid)) statusValues, err := readProcessStatus(filepath.Join(base, "status")) if err != nil { return status.ProcessInfo{}, err } ppid, _ := strconv.Atoi(statusValues["PPid"]) rss := parseKB(statusValues["VmRSS"]) vms := parseKB(statusValues["VmSize"]) username := "" if uid := firstField(statusValues["Uid"]); uid != "" { if u, err := user.LookupId(uid); err == nil { username = u.Username } } cmdline := "" if data, err := os.ReadFile(filepath.Join(base, "cmdline")); err == nil { cmdline = strings.Trim(strings.ReplaceAll(string(data), "\x00", " "), " ") } exe, _ := os.Readlink(filepath.Join(base, "exe")) statValues, _ := readProcessStat(filepath.Join(base, "stat")) return status.ProcessInfo{ PID: pid, PPID: ppid, Name: statusValues["Name"], Exe: exe, Cmdline: cmdline, Username: username, Status: statusValues["State"], StartTime: statValues.StartTime, MemoryRSS: rss, MemoryVMS: vms, }, nil } // processStat 包含从 /proc//stat 解析的进程计数。 type processStat struct { StartTime int64 } // readProcessStat 从路径解析 Linux 进程 stat 计数。 func readProcessStat(path string) (processStat, error) { data, err := os.ReadFile(path) if err != nil { return processStat{}, err } startTime, err := parseProcessStatStartTime(string(data)) if err != nil { return processStat{}, err } return processStat{StartTime: startTime}, nil } // readProcessStatus 从 /proc//status 解析键值字段。 func readProcessStatus(path string) (map[string]string, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() values := map[string]string{} scanner := bufio.NewScanner(file) for scanner.Scan() { parts := strings.SplitN(scanner.Text(), ":", 2) if len(parts) == 2 { values[parts[0]] = strings.TrimSpace(parts[1]) } } return values, scanner.Err() } // socketInodes 将 Linux socket inode 映射到尽力获取的所属进程数据。 func socketInodes() (map[string]status.ProcessInfo, error) { out := map[string]status.ProcessInfo{} var readErrors int _ = filepath.WalkDir("/proc", func(path string, d fs.DirEntry, err error) error { if err != nil || !d.IsDir() || filepath.Dir(path) != "/proc" { return nil } pid, err := strconv.Atoi(filepath.Base(path)) if err != nil { return nil } process, _ := readProcess(pid) fdDir := filepath.Join(path, "fd") entries, err := os.ReadDir(fdDir) if err != nil { readErrors++ return nil } for _, entry := range entries { target, err := os.Readlink(filepath.Join(fdDir, entry.Name())) if err != nil || !strings.HasPrefix(target, "socket:[") { continue } inode := strings.TrimSuffix(strings.TrimPrefix(target, "socket:["), "]") out[inode] = process } return nil }) if readErrors > 0 { return out, fmt.Errorf("%d process fd directories could not be read", readErrors) } return out, nil } // readTCPListeners 解析 Linux TCP 表并返回处于 LISTEN 状态的套接字。 func readTCPListeners(path string, family string, inodes map[string]status.ProcessInfo) ([]status.TCPListenerInfo, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() var items []status.TCPListenerInfo scanner := bufio.NewScanner(file) if scanner.Scan() { // 跳过表头行。 } for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) < 10 || fields[3] != "0A" { continue } ip, port, err := parseAddress(fields[1], family) if err != nil { continue } process := inodes[fields[9]] items = append(items, status.TCPListenerInfo{ IP: ip, Port: port, PID: process.PID, ProcessName: process.Name, ProcessExe: process.Exe, Family: family, }) } return items, scanner.Err() } // readCPUUsagePercent 在短间隔内采样 /proc/stat 并返回尽力估算的 CPU 使用率。 func readCPUUsagePercent(ctx context.Context) float64 { prevData, err := os.ReadFile("/proc/stat") if err != nil { return 0 } prevIdle, prevTotal, err := parseCPUTimes(string(prevData)) if err != nil { return 0 } select { case <-ctx.Done(): return 0 case <-time.After(100 * time.Millisecond): } nextData, err := os.ReadFile("/proc/stat") if err != nil { return 0 } nextIdle, nextTotal, err := parseCPUTimes(string(nextData)) if err != nil { return 0 } return cpuUsagePercent(prevIdle, prevTotal, nextIdle, nextTotal) } // parseAddress 将 Linux /proc/net/tcp 地址解码为 IP 与端口。 func parseAddress(raw string, family string) (string, int, error) { parts := strings.Split(raw, ":") if len(parts) != 2 { return "", 0, fmt.Errorf("invalid address %q", raw) } port64, err := strconv.ParseInt(parts[1], 16, 32) if err != nil { return "", 0, err } bytes, err := hex.DecodeString(parts[0]) if err != nil { return "", 0, err } if family == "ipv4" && len(bytes) == 4 { return net.IPv4(bytes[3], bytes[2], bytes[1], bytes[0]).String(), int(port64), nil } if family == "ipv6" && len(bytes) == 16 { reordered := make([]byte, 16) for i := 0; i < 16; i += 4 { reordered[i], reordered[i+1], reordered[i+2], reordered[i+3] = bytes[i+3], bytes[i+2], bytes[i+1], bytes[i] } return net.IP(reordered).String(), int(port64), nil } return "", 0, fmt.Errorf("invalid %s address", family) } // parseProcessStatStartTime 从 Linux /proc//stat 内容提取第 22 字段。 func parseProcessStatStartTime(raw string) (int64, error) { end := strings.LastIndex(raw, ")") if end < 0 || end+2 >= len(raw) { return 0, fmt.Errorf("invalid process stat") } fields := strings.Fields(raw[end+2:]) if len(fields) < 20 { return 0, fmt.Errorf("process stat has %d fields after comm", len(fields)) } return strconv.ParseInt(fields[19], 10, 64) } // parseCPUTimes 从 /proc/stat 首行 cpu 提取空闲与总 jiffies 计数。 func parseCPUTimes(raw string) (idle uint64, total uint64, err error) { scanner := bufio.NewScanner(strings.NewReader(raw)) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) == 0 || fields[0] != "cpu" { continue } if len(fields) < 5 { return 0, 0, fmt.Errorf("cpu line has %d fields", len(fields)) } for i, field := range fields[1:] { value, err := strconv.ParseUint(field, 10, 64) if err != nil { return 0, 0, err } total += value if i == 3 || i == 4 { idle += value } } return idle, total, nil } if err := scanner.Err(); err != nil { return 0, 0, err } return 0, 0, fmt.Errorf("cpu line not found") } // cpuUsagePercent 根据两次 /proc/stat 采样计算非空闲 CPU 百分比。 func cpuUsagePercent(prevIdle, prevTotal, nextIdle, nextTotal uint64) float64 { totalDelta := nextTotal - prevTotal if totalDelta == 0 { return 0 } idleDelta := nextIdle - prevIdle if idleDelta >= totalDelta { return 0 } return float64(totalDelta-idleDelta) / float64(totalDelta) * 100 } // parseKB 将 raw 中首个以 KiB 表示的数值转换为字节。 func parseKB(raw string) uint64 { value, _ := strconv.ParseUint(firstField(raw), 10, 64) return value * 1024 } // firstField 返回 raw 中首个以空白分隔的字段。 func firstField(raw string) string { fields := strings.Fields(raw) if len(fields) == 0 { return "" } return fields[0] } // sortedKeys 从字符串集合返回确定顺序的排序键列表。 func sortedKeys(values map[string]struct{}) []string { keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Strings(keys) return keys }