diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index 91a074c..26d4d36 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -35,6 +35,7 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/websocket" agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker" agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" + "github.com/rcourtman/pulse-go-rewrite/pkg/fsfilters" "github.com/rcourtman/pulse-go-rewrite/pkg/pbs" "github.com/rcourtman/pulse-go-rewrite/pkg/pmg" "github.com/rcourtman/pulse-go-rewrite/pkg/proxmox" @@ -6585,62 +6586,25 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam for _, fs := range fsInfo { // Skip special filesystems and mounts - skipReasons := []string{} - reasonReadOnly := "" - shouldSkip := false - - // Check filesystem type - fsTypeLower := strings.ToLower(fs.Type) - if reason, skip := readOnlyFilesystemReason(fs.Type, fs.TotalBytes, fs.UsedBytes); skip { - skipReasons = append(skipReasons, fmt.Sprintf("read-only-%s", reason)) - reasonReadOnly = reason - shouldSkip = true - } - if fs.Type == "tmpfs" || fs.Type == "devtmpfs" || - fs.Type == "cgroup" || fs.Type == "cgroup2" || - fs.Type == "sysfs" || fs.Type == "proc" || - fs.Type == "devpts" || fs.Type == "securityfs" || - fs.Type == "debugfs" || fs.Type == "tracefs" || - fs.Type == "fusectl" || fs.Type == "configfs" || - fs.Type == "pstore" || fs.Type == "hugetlbfs" || - fs.Type == "mqueue" || fs.Type == "bpf" || - strings.Contains(fsTypeLower, "fuse") || // Skip FUSE mounts (often network/special) - strings.Contains(fsTypeLower, "9p") || // Skip 9p mounts (VM shared folders) - strings.Contains(fsTypeLower, "nfs") || // Skip NFS mounts - strings.Contains(fsTypeLower, "cifs") || // Skip CIFS/SMB mounts - strings.Contains(fsTypeLower, "smb") { // Skip SMB mounts - skipReasons = append(skipReasons, "special-fs-type") - shouldSkip = true - } - - // Check mountpoint patterns - if strings.HasPrefix(fs.Mountpoint, "/dev") || - strings.HasPrefix(fs.Mountpoint, "/proc") || - strings.HasPrefix(fs.Mountpoint, "/sys") || - strings.HasPrefix(fs.Mountpoint, "/run") || - strings.HasPrefix(fs.Mountpoint, "/var/lib/docker") || // Skip Docker volumes - strings.HasPrefix(fs.Mountpoint, "/snap") || // Skip snap mounts - fs.Mountpoint == "/boot/efi" || - fs.Mountpoint == "System Reserved" || // Windows System Reserved partition - strings.Contains(fs.Mountpoint, "System Reserved") { // Various Windows reserved formats - skipReasons = append(skipReasons, "special-mountpoint") - shouldSkip = true - } - + shouldSkip, reasons := fsfilters.ShouldSkipFilesystem(fs.Type, fs.Mountpoint, fs.TotalBytes, fs.UsedBytes) if shouldSkip { - if reasonReadOnly != "" { - log.Debug(). - Str("instance", instanceName). - Str("vm", res.Name). - Int("vmid", res.VMID). - Str("mountpoint", fs.Mountpoint). - Str("type", fs.Type). - Float64("total_gb", float64(fs.TotalBytes)/1073741824). - Float64("used_gb", float64(fs.UsedBytes)/1073741824). - Msg("Skipping read-only filesystem from disk aggregation") + // Check if any reason is read-only for detailed logging + for _, r := range reasons { + if strings.HasPrefix(r, "read-only-") { + log.Debug(). + Str("instance", instanceName). + Str("vm", res.Name). + Int("vmid", res.VMID). + Str("mountpoint", fs.Mountpoint). + Str("type", fs.Type). + Float64("total_gb", float64(fs.TotalBytes)/1073741824). + Float64("used_gb", float64(fs.UsedBytes)/1073741824). + Msg("Skipping read-only filesystem from disk aggregation") + break + } } skippedFS = append(skippedFS, fmt.Sprintf("%s(%s,%s)", - fs.Mountpoint, fs.Type, strings.Join(skipReasons, ","))) + fs.Mountpoint, fs.Type, strings.Join(reasons, ","))) continue } diff --git a/pkg/fsfilters/filters.go b/pkg/fsfilters/filters.go index e725523..93f2fc7 100644 --- a/pkg/fsfilters/filters.go +++ b/pkg/fsfilters/filters.go @@ -54,3 +54,83 @@ func ShouldIgnoreReadOnlyFilesystem(fsType string, totalBytes, usedBytes uint64) _, skip := ReadOnlyFilesystemReason(fsType, totalBytes, usedBytes) return skip } + +// virtualFSTypes are filesystem types that represent virtual/pseudo filesystems +// which should not be counted toward disk usage. +var virtualFSTypes = map[string]bool{ + "tmpfs": true, + "devtmpfs": true, + "cgroup": true, + "cgroup2": true, + "sysfs": true, + "proc": true, + "devpts": true, + "securityfs": true, + "debugfs": true, + "tracefs": true, + "fusectl": true, + "configfs": true, + "pstore": true, + "hugetlbfs": true, + "mqueue": true, + "bpf": true, +} + +// networkFSPatterns are substrings that indicate network/remote filesystems. +var networkFSPatterns = []string{"fuse", "9p", "nfs", "cifs", "smb"} + +// specialMountPrefixes are mountpoint prefixes that indicate system mounts. +var specialMountPrefixes = []string{ + "/dev", + "/proc", + "/sys", + "/run", + "/var/lib/docker", + "/snap", +} + +// ShouldSkipFilesystem determines if a filesystem should be excluded from disk +// usage aggregation. It checks for read-only filesystems, virtual/pseudo filesystems, +// network mounts, and special system mountpoints. Returns skip=true if the filesystem +// should be excluded, along with a list of reason strings. +func ShouldSkipFilesystem(fsType, mountpoint string, totalBytes, usedBytes uint64) (skip bool, reasons []string) { + fsTypeLower := strings.ToLower(strings.TrimSpace(fsType)) + + // Check read-only filesystems (existing logic) + if reason, isReadOnly := ReadOnlyFilesystemReason(fsType, totalBytes, usedBytes); isReadOnly { + reasons = append(reasons, "read-only-"+reason) + } + + // Check virtual filesystem types + if virtualFSTypes[fsTypeLower] { + reasons = append(reasons, "special-fs-type") + } + + // Check network filesystem patterns + for _, pattern := range networkFSPatterns { + if strings.Contains(fsTypeLower, pattern) { + reasons = append(reasons, "special-fs-type") + break + } + } + + // Check special mountpoint prefixes + for _, prefix := range specialMountPrefixes { + if strings.HasPrefix(mountpoint, prefix) { + reasons = append(reasons, "special-mountpoint") + break + } + } + + // Check specific special mountpoints + if mountpoint == "/boot/efi" { + reasons = append(reasons, "special-mountpoint") + } + + // Windows System Reserved partition + if mountpoint == "System Reserved" || strings.Contains(mountpoint, "System Reserved") { + reasons = append(reasons, "special-mountpoint") + } + + return len(reasons) > 0, reasons +} diff --git a/pkg/fsfilters/filters_test.go b/pkg/fsfilters/filters_test.go index d3ec258..7ee28cb 100644 --- a/pkg/fsfilters/filters_test.go +++ b/pkg/fsfilters/filters_test.go @@ -141,3 +141,65 @@ func TestShouldIgnoreReadOnlyFilesystem(t *testing.T) { t.Fatalf("expected ext4 to be included") } } + +func TestShouldSkipFilesystem(t *testing.T) { + tests := []struct { + name string + fsType string + mountpoint string + totalBytes uint64 + usedBytes uint64 + expectSkip bool + }{ + // Virtual filesystem types + {"tmpfs", "tmpfs", "/tmp", 1024, 512, true}, + {"devtmpfs", "devtmpfs", "/dev", 1024, 100, true}, + {"cgroup2", "cgroup2", "/sys/fs/cgroup", 0, 0, true}, + {"sysfs", "sysfs", "/sys", 0, 0, true}, + {"proc", "proc", "/proc", 0, 0, true}, + {"virtual fs case insensitive", "TMPFS", "/tmp", 1024, 512, true}, + + // Network filesystem types + {"nfs mount", "nfs4", "/mnt/nas", 1000000, 500000, true}, + {"cifs mount", "cifs", "/mnt/share", 1000000, 500000, true}, + {"fuse.sshfs", "fuse.sshfs", "/mnt/remote", 1000000, 500000, true}, + {"9p VM shared folder", "9p", "/mnt/host", 1000000, 500000, true}, + + // Special mountpoint prefixes + {"/dev prefix", "ext4", "/dev/shm", 1024, 100, true}, + {"/proc prefix", "ext4", "/proc/sys", 1024, 100, true}, + {"/sys prefix", "ext4", "/sys/kernel", 1024, 100, true}, + {"/run prefix", "ext4", "/run/user/1000", 1024, 100, true}, + {"/var/lib/docker", "ext4", "/var/lib/docker/overlay2", 1000000, 500000, true}, + {"/snap prefix", "ext4", "/snap/core/12345", 1000000, 500000, true}, + {"/boot/efi exact", "vfat", "/boot/efi", 512 * 1024 * 1024, 50 * 1024 * 1024, true}, + + // Windows paths + {"Windows System Reserved", "NTFS", "System Reserved", 500 * 1024 * 1024, 100 * 1024 * 1024, true}, + {"Windows C drive - should NOT skip", "NTFS", "C:\\", 500 * 1024 * 1024 * 1024, 200 * 1024 * 1024 * 1024, false}, + {"Windows D drive - should NOT skip", "NTFS", "D:\\", 1000 * 1024 * 1024 * 1024, 500 * 1024 * 1024 * 1024, false}, + + // Regular filesystems that should NOT be skipped + {"ext4 root", "ext4", "/", 100 * 1024 * 1024 * 1024, 50 * 1024 * 1024 * 1024, false}, + {"xfs data", "xfs", "/data", 500 * 1024 * 1024 * 1024, 200 * 1024 * 1024 * 1024, false}, + {"btrfs home", "btrfs", "/home", 200 * 1024 * 1024 * 1024, 100 * 1024 * 1024 * 1024, false}, + {"zfs pool", "zfs", "/tank", 10 * 1024 * 1024 * 1024 * 1024, 5 * 1024 * 1024 * 1024 * 1024, false}, + + // Edge cases + {"empty fsType", "", "/mnt/data", 1000000, 500000, false}, + {"empty mountpoint", "ext4", "", 1000000, 500000, false}, + {"whitespace fsType", " tmpfs ", "/tmp", 1024, 512, true}, + + // Read-only filesystems (should still work through new function) + {"squashfs via ShouldSkipFilesystem", "squashfs", "/snap/firefox/123", 100 * 1024 * 1024, 100 * 1024 * 1024, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + skip, reasons := ShouldSkipFilesystem(tc.fsType, tc.mountpoint, tc.totalBytes, tc.usedBytes) + if skip != tc.expectSkip { + t.Errorf("expected skip=%t, got skip=%t (reasons: %v)", tc.expectSkip, skip, reasons) + } + }) + } +}