| // Copyright The Prometheus Authors |
| // Licensed 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. |
| |
| package btrfs |
| |
| import ( |
| "bufio" |
| "fmt" |
| "os" |
| "path" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/prometheus/procfs" |
| "github.com/prometheus/procfs/internal/fs" |
| "github.com/prometheus/procfs/internal/util" |
| ) |
| |
| // FS represents the pseudo-filesystem sys, which provides an interface to |
| // kernel data structures. |
| type FS struct { |
| sys *fs.FS |
| } |
| |
| // NewDefaultFS returns a new Bcache using the default sys fs mount point. It will error |
| // if the mount point can't be read. |
| func NewDefaultFS() (FS, error) { |
| return NewFS(fs.DefaultSysMountPoint) |
| } |
| |
| // NewFS returns a new Btrfs filesystem using the given sys fs mount point. It will error |
| // if the mount point can't be read. |
| func NewFS(mountPoint string) (FS, error) { |
| if strings.TrimSpace(mountPoint) == "" { |
| mountPoint = fs.DefaultSysMountPoint |
| } |
| sys, err := fs.NewFS(mountPoint) |
| if err != nil { |
| return FS{}, err |
| } |
| return FS{&sys}, nil |
| } |
| |
| // Stats retrieves Btrfs filesystem runtime statistics for each mounted Btrfs filesystem. |
| func (fs FS) Stats() ([]*Stats, error) { |
| matches, err := filepath.Glob(fs.sys.Path("fs/btrfs/*-*")) |
| if err != nil { |
| return nil, err |
| } |
| |
| stats := make([]*Stats, 0, len(matches)) |
| for _, uuidPath := range matches { |
| s, err := GetStats(uuidPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Set the UUID from the path when it could not be retrieved from the filesystem. |
| if s.UUID == "" { |
| s.UUID = filepath.Base(uuidPath) |
| } |
| |
| stats = append(stats, s) |
| } |
| |
| return stats, nil |
| } |
| |
| // GetStats collects all Btrfs statistics from sysfs. |
| func GetStats(uuidPath string) (*Stats, error) { |
| r := &reader{path: uuidPath} |
| s := r.readFilesystemStats() |
| if r.err != nil { |
| return nil, r.err |
| } |
| |
| return s, nil |
| } |
| |
| type reader struct { |
| path string |
| err error |
| devCount int |
| } |
| |
| // readFile reads a file relative to the path of the reader. |
| // Non-existing files are ignored. |
| func (r *reader) readFile(n string) string { |
| b, err := util.SysReadFile(path.Join(r.path, n)) |
| if err != nil && !os.IsNotExist(err) { |
| r.err = err |
| } |
| return strings.TrimSpace(string(b)) |
| } |
| |
| // readValues reads a number of numerical values into an uint64 slice. |
| func (r *reader) readValue(n string) (v uint64) { |
| // Read value from file |
| s := r.readFile(n) |
| if r.err != nil { |
| return |
| } |
| |
| // Convert number |
| v, _ = strconv.ParseUint(s, 10, 64) |
| return |
| } |
| |
| // listFiles returns a list of files for a directory of the reader. |
| func (r *reader) listFiles(p string) []string { |
| files, err := os.ReadDir(path.Join(r.path, p)) |
| if err != nil { |
| r.err = err |
| return nil |
| } |
| |
| names := make([]string, len(files)) |
| for i, f := range files { |
| names[i] = f.Name() |
| } |
| return names |
| } |
| |
| // readAllocationStats reads Btrfs allocation data for the current path. |
| func (r *reader) readAllocationStats(d string) (a *AllocationStats) { |
| // Create a reader for this subdirectory |
| sr := &reader{path: path.Join(r.path, d), devCount: r.devCount} |
| |
| // Get the stats |
| a = &AllocationStats{ |
| // Read basic allocation stats |
| MayUseBytes: sr.readValue("bytes_may_use"), |
| PinnedBytes: sr.readValue("bytes_pinned"), |
| ReadOnlyBytes: sr.readValue("bytes_readonly"), |
| ReservedBytes: sr.readValue("bytes_reserved"), |
| UsedBytes: sr.readValue("bytes_used"), |
| DiskUsedBytes: sr.readValue("disk_used"), |
| DiskTotalBytes: sr.readValue("disk_total"), |
| Flags: sr.readValue("flags"), |
| TotalBytes: sr.readValue("total_bytes"), |
| TotalPinnedBytes: sr.readValue("total_bytes_pinned"), |
| Layouts: sr.readLayouts(), |
| } |
| |
| // Pass any error back |
| r.err = sr.err |
| |
| return |
| } |
| |
| // readLayouts reads all Btrfs layout statistics for the current path. |
| func (r *reader) readLayouts() map[string]*LayoutUsage { |
| files, err := os.ReadDir(r.path) |
| if err != nil { |
| r.err = err |
| return nil |
| } |
| |
| m := make(map[string]*LayoutUsage) |
| for _, f := range files { |
| if f.IsDir() { |
| m[f.Name()] = r.readLayout(f.Name()) |
| } |
| } |
| |
| return m |
| } |
| |
| // readLayout reads the Btrfs layout statistics for an allocation layout. |
| func (r *reader) readLayout(p string) (l *LayoutUsage) { |
| l = new(LayoutUsage) |
| l.TotalBytes = r.readValue(path.Join(p, "total_bytes")) |
| l.UsedBytes = r.readValue(path.Join(p, "used_bytes")) |
| l.Ratio = r.calcRatio(p) |
| |
| return |
| } |
| |
| // calcRatio returns the calculated ratio for a layout mode. |
| func (r *reader) calcRatio(p string) float64 { |
| switch p { |
| case "single", "raid0": |
| return 1 |
| case "dup", "raid1", "raid10": |
| return 2 |
| case "raid1c3": |
| return 3 |
| case "raid1c4": |
| return 4 |
| case "raid5": |
| return float64(r.devCount) / (float64(r.devCount) - 1) |
| case "raid6": |
| return float64(r.devCount) / (float64(r.devCount) - 2) |
| default: |
| return 0 |
| } |
| } |
| |
| // readDeviceInfo returns the information for all devices associated with this filesystem. |
| func (r *reader) readDeviceInfo(d string) map[string]*Device { |
| devs := r.listFiles(d) |
| info := make(map[string]*Device, len(devs)) |
| for _, n := range devs { |
| info[n] = &Device{ |
| Size: procfs.SectorSize * r.readValue("devices/"+n+"/size"), |
| } |
| } |
| |
| return info |
| } |
| |
| // readFilesystemStats reads Btrfs statistics for a filesystem. |
| func (r *reader) readFilesystemStats() (s *Stats) { |
| // First get disk info, and add it to reader |
| devices := r.readDeviceInfo("devices") |
| r.devCount = len(devices) |
| |
| s = &Stats{ |
| // Read basic filesystem information |
| Label: r.readFile("label"), |
| UUID: r.readFile("metadata_uuid"), |
| Features: r.listFiles("features"), |
| CloneAlignment: r.readValue("clone_alignment"), |
| NodeSize: r.readValue("nodesize"), |
| QuotaOverride: r.readValue("quota_override"), |
| SectorSize: r.readValue("sectorsize"), |
| |
| // Device info |
| Devices: devices, |
| |
| // Read allocation data |
| Allocation: Allocation{ |
| GlobalRsvReserved: r.readValue("allocation/global_rsv_reserved"), |
| GlobalRsvSize: r.readValue("allocation/global_rsv_size"), |
| Data: r.readAllocationStats("allocation/data"), |
| Metadata: r.readAllocationStats("allocation/metadata"), |
| System: r.readAllocationStats("allocation/system"), |
| }, |
| |
| // Read commit stats data |
| CommitStats: r.readCommitStats("commit_stats"), |
| } |
| return |
| } |
| |
| // readCommitStats returns the commit_stats information for commit stats metrics. |
| func (r *reader) readCommitStats(p string) CommitStats { |
| stats := CommitStats{} |
| |
| f, err := os.Open(path.Join(r.path, p)) |
| if err != nil { |
| // if commit_stats not found. maybe btrfs version < 6.0 |
| if !os.IsNotExist(err) { |
| r.err = err |
| } |
| return stats |
| } |
| defer f.Close() |
| |
| scanner := bufio.NewScanner(f) |
| |
| for scanner.Scan() { |
| line := scanner.Text() |
| parts := strings.Fields(scanner.Text()) |
| // require <key> <value> |
| if len(parts) != 2 { |
| r.err = fmt.Errorf("invalid commit_stats line %q", line) |
| return stats |
| } |
| |
| value, err := strconv.ParseUint(parts[1], 10, 64) |
| if err != nil { |
| r.err = fmt.Errorf("error parsing commit_stats line: %w", err) |
| return stats |
| } |
| |
| switch metricName := parts[0]; metricName { |
| case "commits": |
| stats.Commits = value |
| case "last_commit_ms": |
| stats.LastCommitMs = value |
| case "max_commit_ms": |
| stats.MaxCommitMs = value |
| case "total_commit_ms": |
| stats.TotalCommitMs = value |
| default: |
| continue |
| } |
| } |
| |
| if err := scanner.Err(); err != nil { |
| r.err = fmt.Errorf("error scanning commit_stats file: %w", err) |
| return stats |
| } |
| |
| return stats |
| } |