Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions collector/filesystem_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
)
}
}
Expand Down Expand Up @@ -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
}
54 changes: 52 additions & 2 deletions collector/filesystem_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -84,6 +86,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"
Expand Down Expand Up @@ -187,10 +194,47 @@ func mountPointDetails(logger *slog.Logger) ([]filesystemLabels, error) {
}
defer file.Close()

return parseFilesystemLabels(file)
UUIDMap := buildUUIDMap(logger)

return parseFilesystemLabels(file, UUIDMap)
}

// 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(r io.Reader) ([]filesystemLabels, error) {
func parseFilesystemLabels(r io.Reader, UUIDMap map[string]string) ([]filesystemLabels, error) {
var filesystems []filesystemLabels

scanner := bufio.NewScanner(r)
Expand All @@ -212,6 +256,11 @@ func parseFilesystemLabels(r io.Reader) ([]filesystemLabels, error) {
m++
}

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).
parts[4] = strings.ReplaceAll(parts[4], "\\040", " ")
Expand All @@ -221,6 +270,7 @@ func parseFilesystemLabels(r io.Reader) ([]filesystemLabels, error) {
device: parts[m+3],
mountPoint: rootfsStripPrefix(parts[4]),
fsType: parts[m+2],
fsUUID: fsUUID,
mountOptions: parts[5],
superOptions: parts[10],
major: strconv.Itoa(major),
Expand Down
12 changes: 7 additions & 5 deletions collector/filesystem_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ import (

func Test_parseFilesystemLabelsError(t *testing.T) {
tests := []struct {
name string
in string
name string
in string
uuids map[string]string
}{
{
name: "too few fields",
in: "hello world",
name: "too few fields",
in: "hello world",
uuids: map[string]string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := parseFilesystemLabels(strings.NewReader(tt.in)); err == nil {
if _, err := parseFilesystemLabels(strings.NewReader(tt.in), tt.uuids); err == nil {
t.Fatal("expected an error, but none occurred")
}
})
Expand Down
5 changes: 5 additions & 0 deletions collector/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down