//go:build !windows // Package collector 提供服务器各项系统指标的采集能力。 // 本文件实现对所有已挂载的真实文件系统执行 statfs,采集磁盘使用情况。 // 使用 golang.org/x/sys/unix 的 Statfs,兼容 Linux 与 macOS。 // // 署名 Logan.Li package collector import ( "bufio" "fmt" "os" "runtime" "strings" "golang.org/x/sys/unix" "ezops/agent/model" ) // DiskCollector 磁盘使用率采集器 type DiskCollector struct{} // NewDiskCollector 创建磁盘采集器实例 func NewDiskCollector() *DiskCollector { return &DiskCollector{} } // mount 描述一个挂载点信息 type mount struct { device string mountpoint string fstype string } // realFilesystems 允许采集的"真实"文件系统类型白名单, // 用于排除 tmpfs/proc/sysfs 等伪文件系统,避免产生噪音数据。 var realFilesystems = map[string]bool{ "ext2": true, "ext3": true, "ext4": true, "xfs": true, "btrfs": true, "zfs": true, "apfs": true, "hfs": true, "exfat": true, "ntfs": true, "vfat": true, "fat32": true, "f2fs": true, "jfs": true, "reiserfs": true, "ufs": true, } // Collect 采集所有真实文件系统挂载点的磁盘使用情况 func (d *DiskCollector) Collect() ([]model.DiskStat, error) { mounts, err := d.getMounts() if err != nil { return nil, fmt.Errorf("获取挂载点失败: %w", err) } disks := make([]model.DiskStat, 0, len(mounts)) seen := make(map[string]bool) // 按挂载点去重 for _, m := range mounts { if !realFilesystems[m.fstype] { continue } if seen[m.mountpoint] { continue } seen[m.mountpoint] = true var fs unix.Statfs_t if err := unix.Statfs(m.mountpoint, &fs); err != nil { // 单个挂载点采集失败不影响其余挂载点 continue } bsize := uint64(fs.Bsize) total := fs.Blocks * bsize free := fs.Bfree * bsize if total == 0 { continue } used := total - free // inode 使用率 var inodePct float64 if fs.Files > 0 { inodePct = float64(fs.Files-fs.Ffree) / float64(fs.Files) * 100.0 if inodePct < 0 { inodePct = 0 } } disks = append(disks, model.DiskStat{ Mount: m.mountpoint, Device: m.device, TotalGB: bytesToGB(total), UsedGB: bytesToGB(used), UsagePercent: float64(used) / float64(total) * 100.0, InodeUsagePct: inodePct, }) } return disks, nil } // getMounts 获取挂载点列表。 // - Linux: 解析 /proc/mounts // - macOS(开发环境): 返回根分区 func (d *DiskCollector) getMounts() ([]mount, error) { if runtime.GOOS != "linux" { // 开发环境(macOS)下提供根分区以便本地调试 return []mount{{device: "/dev/disk1", mountpoint: "/", fstype: "apfs"}}, nil } f, err := os.Open("/proc/mounts") if err != nil { return nil, err } defer f.Close() var mounts []mount scanner := bufio.NewScanner(f) // 挂载点路径可能较长,扩大缓冲区 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { // 形如: /dev/sda1 / ext4 rw,relatime 0 0 fields := strings.Fields(scanner.Text()) if len(fields) < 3 { continue } mounts = append(mounts, mount{ device: fields[0], mountpoint: unescapeMount(fields[1]), fstype: fields[2], }) } return mounts, scanner.Err() } // unescapeMount 还原 /proc/mounts 中被转义的挂载点路径(如 \040 -> 空格) func unescapeMount(s string) string { return strings.ReplaceAll(s, "\\040", " ") } // bytesToGB 字节数转 GB func bytesToGB(b uint64) float64 { const gb = 1024.0 * 1024.0 * 1024.0 return float64(b) / gb }