diff --git a/collector/filesystem_common.go b/collector/filesystem_common.go index 1e61e8e705..fee64d5831 100644 --- a/collector/filesystem_common.go +++ b/collector/filesystem_common.go @@ -67,12 +67,26 @@ var ( "Regexp of filesystem types to exclude for filesystem collector. (mutually exclusive to fs-types-exclude)", ).String() - filesystemLabelNames = []string{"device", "mountpoint", "fstype", "device_error"} + fsUUIDsExcludeSet bool + fsUUIDsExclude = kingpin.Flag( + "collector.filesystem.fs-uuids-exclude", + "Regexp of filesystem UUIDs to exclude for filesystem collector. (mutually exclusive to fs-uuids-include)", + ).Default("").PreAction(func(c *kingpin.ParseContext) error { + fsUUIDsExcludeSet = true + return nil + }).String() + fsUUIDsInclude = kingpin.Flag( + "collector.filesystem.fs-uuids-include", + "Regexp of filesystem UUIDs to include for filesystem collector. (mutually exclusive to fs-uuids-exclude)", + ).String() + + filesystemLabelNames = []string{"device", "mountpoint", "fstype", "fsuuid", "device_error"} ) type filesystemCollector struct { mountPointFilter deviceFilter fsTypeFilter deviceFilter + fsUUIDFilter deviceFilter sizeDesc, freeDesc, availDesc *prometheus.Desc filesDesc, filesFreeDesc *prometheus.Desc purgeableDesc *prometheus.Desc @@ -82,7 +96,7 @@ type filesystemCollector struct { } type filesystemLabels struct { - device, mountPoint, fsType, mountOptions, superOptions, deviceError, major, minor string + device, mountPoint, fsType, fsUUID, mountOptions, superOptions, deviceError, major, minor string } type filesystemStats struct { @@ -166,9 +180,15 @@ func NewFilesystemCollector(logger *slog.Logger) (Collector, error) { return nil, fmt.Errorf("unable to parse fs types filter flags: %w", err) } + fsUUIDFilter, err := newFSUUIDFilter(logger) + if err != nil { + return nil, fmt.Errorf("unable to parse fs UUIDs filter flags: %w", err) + } + return &filesystemCollector{ mountPointFilter: mountPointFilter, fsTypeFilter: fsTypeFilter, + fsUUIDFilter: fsUUIDFilter, sizeDesc: sizeDesc, freeDesc: freeDesc, availDesc: availDesc, @@ -197,11 +217,11 @@ func (c *filesystemCollector) Update(ch chan<- prometheus.Metric) error { ch <- prometheus.MustNewConstMetric( c.deviceErrorDesc, prometheus.GaugeValue, - s.deviceError, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.deviceError, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) ch <- prometheus.MustNewConstMetric( c.roDesc, prometheus.GaugeValue, - s.ro, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.ro, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) if s.deviceError > 0 { @@ -210,23 +230,23 @@ func (c *filesystemCollector) Update(ch chan<- prometheus.Metric) error { ch <- prometheus.MustNewConstMetric( c.sizeDesc, prometheus.GaugeValue, - s.size, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.size, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) ch <- prometheus.MustNewConstMetric( c.freeDesc, prometheus.GaugeValue, - s.free, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.free, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) ch <- prometheus.MustNewConstMetric( c.availDesc, prometheus.GaugeValue, - s.avail, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.avail, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) ch <- prometheus.MustNewConstMetric( c.filesDesc, prometheus.GaugeValue, - s.files, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.files, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) ch <- prometheus.MustNewConstMetric( c.filesFreeDesc, prometheus.GaugeValue, - s.filesFree, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.filesFree, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) ch <- prometheus.MustNewConstMetric( c.mountInfoDesc, prometheus.GaugeValue, @@ -235,7 +255,7 @@ func (c *filesystemCollector) Update(ch chan<- prometheus.Metric) error { if s.purgeable >= 0 { ch <- prometheus.MustNewConstMetric( c.purgeableDesc, prometheus.GaugeValue, - s.purgeable, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.deviceError, + s.purgeable, s.labels.device, s.labels.mountPoint, s.labels.fsType, s.labels.fsUUID, s.labels.deviceError, ) } } @@ -299,3 +319,22 @@ func newFSTypeFilter(logger *slog.Logger) (deviceFilter, error) { return newDeviceFilter(*fsTypesExclude, *fsTypesInclude), nil } + +func newFSUUIDFilter(logger *slog.Logger) (deviceFilter, error) { + if *fsUUIDsInclude != "" && !fsUUIDsExcludeSet { + logger.Debug("fs-uuids-exclude flag not set when fs-uuids-include flag is set, assuming include is desired") + *fsUUIDsExclude = "" + } + if *fsUUIDsExclude != "" && *fsUUIDsInclude != "" { + return deviceFilter{}, errors.New("--collector.filesystem.fs-uuids-exclude and --collector.filesystem.fs-uuids-include are mutually exclusive") + } + + if *fsUUIDsExclude != "" { + logger.Info("Parsed flag --collector.filesystem.fs-uuids-exclude", "flag", *fsUUIDsExclude) + } + if *fsUUIDsInclude != "" { + logger.Info("Parsed flag --collector.filesystem.fs-uuids-include", "flag", *fsUUIDsInclude) + } + + return newDeviceFilter(*fsUUIDsExclude, *fsUUIDsInclude), nil +} diff --git a/collector/filesystem_linux.go b/collector/filesystem_linux.go index c3e6370b2d..15e2065c23 100644 --- a/collector/filesystem_linux.go +++ b/collector/filesystem_linux.go @@ -20,8 +20,10 @@ import ( "bytes" "errors" "fmt" + "io/fs" "log/slog" "os" + "path/filepath" "slices" "strconv" "strings" @@ -82,6 +84,11 @@ func (c *filesystemCollector) GetStats() ([]filesystemStats, error) { continue } + if c.fsUUIDFilter.ignored(labels.fsUUID) { + c.logger.Debug("Ignoring fs UUID", "uuid", labels.fsUUID) + continue + } + stuckMountsMtx.Lock() if _, ok := stuckMounts[labels.mountPoint]; ok { labels.deviceError = "mountpoint timeout" @@ -193,11 +200,47 @@ func mountPointDetails(logger *slog.Logger) ([]filesystemLabels, error) { if err != nil { return nil, err } + UUIDMap := buildUUIDMap(logger) - return parseFilesystemLabels(mountInfo) + return parseFilesystemLabels(mountInfo, UUIDMap) } -func parseFilesystemLabels(mountInfo []*procfs.MountInfo) ([]filesystemLabels, error) { +// buildUUIDMap builds a map of device major:minor numbers to their filesystem UUIDs. +func buildUUIDMap(logger *slog.Logger) map[string]string { + UUIDMap := map[string]string{} + dir := devDiskFilePath("by-uuid") + _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + logger.Debug("Skipping non-file entry", "path", path, "err", err) + return nil + } + uuid := d.Name() + target, err := os.Readlink(path) + if err != nil { + if t, err := filepath.EvalSymlinks(path); err == nil { + target = t + } else { + logger.Debug("Failed to read symlink", "path", path, "err", err) + return nil + } + } + if !filepath.IsAbs(target) { + target = filepath.Join(filepath.Dir(path), target) + } + var stat unix.Stat_t + if err := unix.Stat(target, &stat); err != nil { + logger.Debug("Failed to stat device", "path", path, "err", err) + return nil + } + maj := unix.Major(uint64(stat.Rdev)) + min := unix.Minor(uint64(stat.Rdev)) + UUIDMap[fmt.Sprintf("%d:%d", maj, min)] = uuid + return nil + }) + return UUIDMap +} + +func parseFilesystemLabels(mountInfo []*procfs.MountInfo, UUIDMap map[string]string) ([]filesystemLabels, error) { var filesystems []filesystemLabels for _, mount := range mountInfo { @@ -207,6 +250,11 @@ func parseFilesystemLabels(mountInfo []*procfs.MountInfo) ([]filesystemLabels, e return nil, fmt.Errorf("malformed mount point MajorMinorVer: %q", mount.MajorMinorVer) } + fsUUID, ok := UUIDMap[fmt.Sprintf("%d:%d", major, minor)] + if !ok { + fsUUID = "" + } + // Ensure we handle the translation of \040 and \011 // as per fstab(5). mount.MountPoint = strings.ReplaceAll(mount.MountPoint, "\\040", " ") @@ -218,6 +266,7 @@ func parseFilesystemLabels(mountInfo []*procfs.MountInfo) ([]filesystemLabels, e fsType: mount.FSType, mountOptions: mountOptionsString(mount.Options), superOptions: mountOptionsString(mount.SuperOptions), + fsUUID: fsUUID, major: strconv.Itoa(major), minor: strconv.Itoa(minor), deviceError: "", diff --git a/collector/filesystem_linux_test.go b/collector/filesystem_linux_test.go index 58b4dc6ba1..3c06c28c85 100644 --- a/collector/filesystem_linux_test.go +++ b/collector/filesystem_linux_test.go @@ -28,8 +28,9 @@ import ( func Test_parseFilesystemLabelsError(t *testing.T) { tests := []struct { - name string - in []*procfs.MountInfo + name string + in []*procfs.MountInfo + uuids map[string]string }{ { name: "malformed Major:Minor", @@ -38,12 +39,13 @@ func Test_parseFilesystemLabelsError(t *testing.T) { MajorMinorVer: "nope", }, }, + uuids: map[string]string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if _, err := parseFilesystemLabels(tt.in); err == nil { + if _, err := parseFilesystemLabels(tt.in, tt.uuids); err == nil { t.Fatal("expected an error, but none occurred") } }) diff --git a/collector/paths.go b/collector/paths.go index 82c941876d..6f14bc9fd9 100644 --- a/collector/paths.go +++ b/collector/paths.go @@ -27,6 +27,7 @@ var ( sysPath = kingpin.Flag("path.sysfs", "sysfs mountpoint.").Default("/sys").String() rootfsPath = kingpin.Flag("path.rootfs", "rootfs mountpoint.").Default("/").String() udevDataPath = kingpin.Flag("path.udev.data", "udev data path.").Default("/run/udev/data").String() + devDiskPath = kingpin.Flag("path.dev.disk", "path to /dev/disk").Default("/dev/disk").String() ) func procFilePath(name string) string { @@ -45,6 +46,10 @@ func udevDataFilePath(name string) string { return filepath.Join(*udevDataPath, name) } +func devDiskFilePath(name string) string { + return filepath.Join(*devDiskPath, name) +} + func rootfsStripPrefix(path string) string { if *rootfsPath == "/" { return path