metric/system/filesystem/filesystem.go (131 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. //go:build darwin || freebsd || linux || openbsd || windows // +build darwin freebsd linux openbsd windows package filesystem import ( "bufio" "fmt" "os" "path/filepath" "runtime" "strings" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/opt" "github.com/elastic/elastic-agent-system-metrics/metric" "github.com/elastic/elastic-agent-system-metrics/metric/system/resolve" ) // FSStat carries the metadata for a given filesystem type FSStat struct { Directory string `struct:"mount_point,omitempty"` Device string `struct:"device_name,omitempty"` Type string `struct:"type,omitempty"` Options string `struct:"options,omitempty"` Flags opt.Uint `struct:"flags,omitempty"` // metrics Total opt.Uint `struct:"total,omitempty"` Free opt.Uint `struct:"free,omitempty"` Avail opt.Uint `struct:"available,omitempty"` Used UsedVals `struct:"used,omitempty"` Files opt.Uint `struct:"files,omitempty"` FreeFiles opt.Uint `struct:"free_files,omitempty"` } // UsedVals wraps the `used` disk metrics type UsedVals struct { Pct opt.Float `struct:"pct,omitempty"` Bytes opt.Uint `struct:"bytes,omitempty"` } // IsZero implements the IsZero interface for go-structform func (u UsedVals) IsZero() bool { return u.Pct.IsZero() && u.Bytes.IsZero() } var debugf = logp.MakeDebug("libbeat.filesystem") func getFSPath(hostfs resolve.Resolver) string { // Do a little work to make sure we don't break anything. // This code would previously just blindly just search for /etc/mtab // This wasn't available on certain containerized workflows, // So default to mtab's symlink of /proc/self/mounts // However, I'm a little skeptical of `self` inside containers, // so if hostfs is set, use /hostfs/proc/mounts if hostfs.IsSet() { return hostfs.ResolveHostFS("/proc/mounts") } return hostfs.ResolveHostFS("/proc/self/mounts") } // GetFilesystems returns a filesystem list filtered by the callback function func GetFilesystems(hostfs resolve.Resolver, filter func(FSStat) bool) ([]FSStat, error) { fs := getFSPath(hostfs) if filter == nil { filter = buildDefaultFilters(hostfs) } //combine user-supplied and built-in filters filterFunc := func(fs FSStat) bool { return avoidFileSystem(fs) && filter(fs) } mounts, err := parseMounts(fs, filterFunc) if err != nil { return nil, fmt.Errorf("error reading mounts: %w", err) } return filterDuplicates(mounts), nil } // Fill out computed stats after the platform-specific code fetches metrics from the OS func (fs *FSStat) fillMetrics() { fs.Used.Bytes = fs.Total.SubtractOrNone(fs.Free) // I'm not sure why this does Used + avail instead of total, but I'm too afraid to change it percTotal := fs.Used.Bytes.ValueOr(0) + fs.Avail.ValueOr(0) if percTotal == 0 { return } perc := float64(fs.Used.Bytes.ValueOr(0)) / float64(percTotal) fs.Used.Pct = opt.FloatWith(metric.Round(perc)) } // DefaultIgnoredTypes tries to guess a sane list of filesystem types that // could be ignored in the running system func DefaultIgnoredTypes(sys resolve.Resolver) []string { // If /proc/filesystems exist, default ignored types are all marked // as nodev types := []string{} fsListFile := sys.ResolveHostFS("/proc/filesystems") if f, err := os.Open(fsListFile); err == nil { scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.Fields(scanner.Text()) if len(line) == 2 && line[0] == "nodev" { types = append(types, line[1]) } } } return types } // BuildFilterWithList returns a filesystem filter with the given list of FS types func BuildFilterWithList(ignored []string) func(FSStat) bool { return func(fs FSStat) bool { for _, fsType := range ignored { if fs.Type == fsType { return false } } return true } } func buildDefaultFilters(hostfs resolve.Resolver) func(FSStat) bool { ignoreType := DefaultIgnoredTypes(hostfs) return BuildFilterWithList(ignoreType) } // If a block device is mounted multiple times (e.g. with some bind mounts), // store it only once, and use the shorter mount point path. func filterDuplicates(fsList []FSStat) []FSStat { devices := make(map[string]FSStat) filtered := []FSStat{} for _, fs := range fsList { // Don't do any further checks on block devices if !filepath.IsAbs(fs.Device) { filtered = append(filtered, fs) continue } if seen, found := devices[fs.Device]; found { if len(fs.Directory) < len(seen.Directory) { devices[fs.Device] = fs } continue } devices[fs.Device] = fs } for _, fs := range devices { filtered = append(filtered, fs) } return filtered } func avoidFileSystem(fs FSStat) bool { // Ignore relative mount points, which are present for example // in /proc/mounts on Linux with network namespaces. if !filepath.IsAbs(fs.Directory) { debugf("Filtering filesystem with relative mountpoint %+v", fs) return false } // Don't do further checks in special devices if !filepath.IsAbs(fs.Device) { return true } // This logic only applies on non-windows machines, the device path logic seems to be different on windows. if runtime.GOOS != "windows" { // If the device name is a directory, this is a bind mount or nullfs, // don't count it as it'd be counting again its parent filesystem. devFileInfo, err := os.Stat(fs.Device) if err != nil { debugf("error stating filesystem: %s", err) } if devFileInfo != nil && devFileInfo.IsDir() { return false } } return true }