| // 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 bcache |
| |
| import ( |
| "bufio" |
| "fmt" |
| "os" |
| "path" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/prometheus/procfs/internal/fs" |
| ) |
| |
| // FS represents the pseudo-filesystem proc, 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 Bcache 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 |
| } |
| fs, err := fs.NewFS(mountPoint) |
| if err != nil { |
| return FS{}, err |
| } |
| return FS{&fs}, nil |
| } |
| |
| // Stats is a wrapper around stats(). |
| // It returns full available statistics. |
| func (fs FS) Stats() ([]*Stats, error) { |
| return fs.stats(true) |
| } |
| |
| // StatsWithoutPriority is a wrapper around stats(). |
| // It ignores priority_stats file, because it is expensive to read. |
| func (fs FS) StatsWithoutPriority() ([]*Stats, error) { |
| return fs.stats(false) |
| } |
| |
| // stats retrieves bcache runtime statistics for each bcache and |
| // priorityStats flag controls if we need to read priority_stats. |
| func (fs FS) stats(priorityStats bool) ([]*Stats, error) { |
| matches, err := filepath.Glob(fs.sys.Path("fs/bcache/*-*")) |
| if err != nil { |
| return nil, err |
| } |
| |
| stats := make([]*Stats, 0, len(matches)) |
| for _, uuidPath := range matches { |
| // "*-*" in glob above indicates the name of the bcache. |
| name := filepath.Base(uuidPath) |
| |
| // stats |
| s, err := GetStats(uuidPath, priorityStats) |
| if err != nil { |
| return nil, err |
| } |
| |
| s.Name = name |
| stats = append(stats, s) |
| } |
| |
| return stats, nil |
| } |
| |
| // ParsePseudoFloat parses the peculiar format produced by bcache's bch_hprint. |
| func parsePseudoFloat(str string) (float64, error) { |
| ss := strings.Split(str, ".") |
| |
| intPart, err := strconv.ParseFloat(ss[0], 64) |
| if err != nil { |
| return 0, err |
| } |
| |
| if len(ss) == 1 { |
| // Pure integers are fine. |
| return intPart, nil |
| } |
| fracPart, err := strconv.ParseFloat(ss[1], 64) |
| if err != nil { |
| return 0, err |
| } |
| // fracPart is a number between 0 and 1023 divided by 100; it is off |
| // by a small amount. Unexpected bumps in time lines may occur because |
| // for bch_hprint .1 != .10 and .10 > .9 (at least up to Linux |
| // v4.12-rc3). |
| |
| // Restore the proper order: |
| fracPart /= 10.24 |
| return intPart + fracPart, nil |
| } |
| |
| // Dehumanize converts a human-readable byte slice into a uint64. |
| func dehumanize(hbytes []byte) (uint64, error) { |
| ll := len(hbytes) |
| if ll == 0 { |
| return 0, fmt.Errorf("zero-length reply") |
| } |
| lastByte := hbytes[ll-1] |
| mul := float64(1) |
| var ( |
| mant float64 |
| err error |
| ) |
| // If lastByte is beyond the range of ASCII digits, it must be a |
| // multiplier. |
| if lastByte > 57 { |
| // Remove multiplier from slice. |
| hbytes = hbytes[:len(hbytes)-1] |
| |
| const ( |
| _ = 1 << (10 * iota) |
| KiB |
| MiB |
| GiB |
| TiB |
| PiB |
| EiB |
| ZiB |
| YiB |
| ) |
| |
| multipliers := map[rune]float64{ |
| // Source for conversion rules: |
| // linux-kernel/drivers/md/bcache/util.c:bch_hprint() |
| 'k': KiB, |
| 'M': MiB, |
| 'G': GiB, |
| 'T': TiB, |
| 'P': PiB, |
| 'E': EiB, |
| 'Z': ZiB, |
| 'Y': YiB, |
| } |
| mul = multipliers[rune(lastByte)] |
| mant, err = parsePseudoFloat(string(hbytes)) |
| if err != nil { |
| return 0, err |
| } |
| } else { |
| // Not humanized by bch_hprint |
| mant, err = strconv.ParseFloat(string(hbytes), 64) |
| if err != nil { |
| return 0, err |
| } |
| } |
| res := uint64(mant * mul) |
| return res, nil |
| } |
| |
| func dehumanizeSigned(str string) (int64, error) { |
| value, err := dehumanize([]byte(strings.TrimPrefix(str, "-"))) |
| if err != nil { |
| return 0, err |
| } |
| if strings.HasPrefix(str, "-") { |
| return int64(-value), nil |
| } |
| return int64(value), nil |
| } |
| |
| type parser struct { |
| uuidPath string |
| subDir string |
| currentDir string |
| err error |
| } |
| |
| func (p *parser) setSubDir(pathElements ...string) { |
| p.subDir = path.Join(pathElements...) |
| p.currentDir = path.Join(p.uuidPath, p.subDir) |
| } |
| |
| // readValues reads a number of numerical values into an uint64 slice. |
| // Non-existing files are ignored. |
| func (p *parser) readValue(fileName string) uint64 { |
| if p.err != nil { |
| return 0 |
| } |
| path := path.Join(p.currentDir, fileName) |
| byt, err := os.ReadFile(path) |
| if err != nil { |
| if !os.IsNotExist(err) { |
| p.err = fmt.Errorf("failed to read: %s", path) |
| } |
| return 0 |
| } |
| // Remove trailing newline. |
| byt = byt[:len(byt)-1] |
| res, err := dehumanize(byt) |
| p.err = err |
| return res |
| } |
| |
| // ParsePriorityStats parses lines from the priority_stats file. |
| func parsePriorityStats(line string, ps *PriorityStats) error { |
| var ( |
| value uint64 |
| err error |
| ) |
| switch { |
| case strings.HasPrefix(line, "Unused:"): |
| fields := strings.Fields(line) |
| rawValue := fields[len(fields)-1] |
| valueStr := strings.TrimSuffix(rawValue, "%") |
| value, err = strconv.ParseUint(valueStr, 10, 64) |
| if err != nil { |
| return err |
| } |
| ps.UnusedPercent = value |
| case strings.HasPrefix(line, "Metadata:"): |
| fields := strings.Fields(line) |
| rawValue := fields[len(fields)-1] |
| valueStr := strings.TrimSuffix(rawValue, "%") |
| value, err = strconv.ParseUint(valueStr, 10, 64) |
| if err != nil { |
| return err |
| } |
| ps.MetadataPercent = value |
| } |
| return nil |
| } |
| |
| // ParseWritebackRateDebug parses lines from the writeback_rate_debug file. |
| func parseWritebackRateDebug(line string, wrd *WritebackRateDebugStats) error { |
| switch { |
| case strings.HasPrefix(line, "rate:"): |
| fields := strings.Fields(line) |
| rawValue := fields[len(fields)-1] |
| valueStr := strings.TrimSuffix(rawValue, "/sec") |
| value, err := dehumanize([]byte(valueStr)) |
| if err != nil { |
| return err |
| } |
| wrd.Rate = value |
| case strings.HasPrefix(line, "dirty:"): |
| fields := strings.Fields(line) |
| valueStr := fields[len(fields)-1] |
| value, err := dehumanize([]byte(valueStr)) |
| if err != nil { |
| return err |
| } |
| wrd.Dirty = value |
| case strings.HasPrefix(line, "target:"): |
| fields := strings.Fields(line) |
| valueStr := fields[len(fields)-1] |
| value, err := dehumanize([]byte(valueStr)) |
| if err != nil { |
| return err |
| } |
| wrd.Target = value |
| case strings.HasPrefix(line, "proportional:"): |
| fields := strings.Fields(line) |
| valueStr := fields[len(fields)-1] |
| value, err := dehumanizeSigned(valueStr) |
| if err != nil { |
| return err |
| } |
| wrd.Proportional = value |
| case strings.HasPrefix(line, "integral:"): |
| fields := strings.Fields(line) |
| valueStr := fields[len(fields)-1] |
| value, err := dehumanizeSigned(valueStr) |
| if err != nil { |
| return err |
| } |
| wrd.Integral = value |
| case strings.HasPrefix(line, "change:"): |
| fields := strings.Fields(line) |
| rawValue := fields[len(fields)-1] |
| valueStr := strings.TrimSuffix(rawValue, "/sec") |
| value, err := dehumanizeSigned(valueStr) |
| if err != nil { |
| return err |
| } |
| wrd.Change = value |
| case strings.HasPrefix(line, "next io:"): |
| fields := strings.Fields(line) |
| rawValue := fields[len(fields)-1] |
| valueStr := strings.TrimSuffix(rawValue, "ms") |
| value, err := strconv.ParseInt(valueStr, 10, 64) |
| if err != nil { |
| return err |
| } |
| wrd.NextIO = value |
| } |
| return nil |
| } |
| |
| func (p *parser) getPriorityStats() PriorityStats { |
| var res PriorityStats |
| |
| if p.err != nil { |
| return res |
| } |
| |
| path := path.Join(p.currentDir, "priority_stats") |
| |
| file, err := os.Open(path) |
| if err != nil { |
| p.err = fmt.Errorf("failed to read: %s", path) |
| return res |
| } |
| defer file.Close() |
| |
| scanner := bufio.NewScanner(file) |
| for scanner.Scan() { |
| err = parsePriorityStats(scanner.Text(), &res) |
| if err != nil { |
| p.err = fmt.Errorf("failed to parse path %q: %w", path, err) |
| return res |
| } |
| } |
| if err := scanner.Err(); err != nil { |
| p.err = fmt.Errorf("failed to parse path %q: %w", path, err) |
| return res |
| } |
| return res |
| } |
| |
| func (p *parser) getWritebackRateDebug() WritebackRateDebugStats { |
| var res WritebackRateDebugStats |
| |
| if p.err != nil { |
| return res |
| } |
| path := path.Join(p.currentDir, "writeback_rate_debug") |
| file, err := os.Open(path) |
| if err != nil { |
| p.err = fmt.Errorf("failed to read: %s", path) |
| return res |
| } |
| defer file.Close() |
| |
| scanner := bufio.NewScanner(file) |
| for scanner.Scan() { |
| err = parseWritebackRateDebug(scanner.Text(), &res) |
| if err != nil { |
| p.err = fmt.Errorf("failed to parse path %q: %w", path, err) |
| return res |
| } |
| } |
| if err := scanner.Err(); err != nil { |
| p.err = fmt.Errorf("failed to parse path %q: %w", path, err) |
| return res |
| } |
| return res |
| } |
| |
| // GetStats collects from sysfs files data tied to one bcache ID. |
| func GetStats(uuidPath string, priorityStats bool) (*Stats, error) { |
| var bs Stats |
| |
| par := parser{uuidPath: uuidPath} |
| |
| // bcache stats |
| |
| // dir <uuidPath> |
| par.setSubDir("") |
| bs.Bcache.AverageKeySize = par.readValue("average_key_size") |
| bs.Bcache.BtreeCacheSize = par.readValue("btree_cache_size") |
| bs.Bcache.CacheAvailablePercent = par.readValue("cache_available_percent") |
| bs.Bcache.Congested = par.readValue("congested") |
| bs.Bcache.RootUsagePercent = par.readValue("root_usage_percent") |
| bs.Bcache.TreeDepth = par.readValue("tree_depth") |
| |
| // bcache stats (internal) |
| |
| // dir <uuidPath>/internal |
| par.setSubDir("internal") |
| bs.Bcache.Internal.ActiveJournalEntries = par.readValue("active_journal_entries") |
| bs.Bcache.Internal.BtreeNodes = par.readValue("btree_nodes") |
| bs.Bcache.Internal.BtreeReadAverageDurationNanoSeconds = par.readValue("btree_read_average_duration_us") |
| bs.Bcache.Internal.CacheReadRaces = par.readValue("cache_read_races") |
| |
| // bcache stats (period) |
| |
| // dir <uuidPath>/stats_five_minute |
| par.setSubDir("stats_five_minute") |
| bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed") |
| bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits") |
| |
| bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed") |
| bs.Bcache.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits") |
| bs.Bcache.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses") |
| bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits") |
| bs.Bcache.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions") |
| bs.Bcache.FiveMin.CacheMisses = par.readValue("cache_misses") |
| bs.Bcache.FiveMin.CacheReadaheads = par.readValue("cache_readaheads") |
| |
| // dir <uuidPath>/stats_total |
| par.setSubDir("stats_total") |
| bs.Bcache.Total.Bypassed = par.readValue("bypassed") |
| bs.Bcache.Total.CacheHits = par.readValue("cache_hits") |
| |
| bs.Bcache.Total.Bypassed = par.readValue("bypassed") |
| bs.Bcache.Total.CacheBypassHits = par.readValue("cache_bypass_hits") |
| bs.Bcache.Total.CacheBypassMisses = par.readValue("cache_bypass_misses") |
| bs.Bcache.Total.CacheHits = par.readValue("cache_hits") |
| bs.Bcache.Total.CacheMissCollisions = par.readValue("cache_miss_collisions") |
| bs.Bcache.Total.CacheMisses = par.readValue("cache_misses") |
| bs.Bcache.Total.CacheReadaheads = par.readValue("cache_readaheads") |
| |
| if par.err != nil { |
| return nil, par.err |
| } |
| |
| // bdev stats |
| |
| reg := path.Join(uuidPath, "bdev[0-9]*") |
| bdevDirs, err := filepath.Glob(reg) |
| if err != nil { |
| return nil, err |
| } |
| |
| bs.Bdevs = make([]BdevStats, len(bdevDirs)) |
| |
| for ii, bdevDir := range bdevDirs { |
| var bds = &bs.Bdevs[ii] |
| |
| bds.Name = filepath.Base(bdevDir) |
| |
| par.setSubDir(bds.Name) |
| bds.DirtyData = par.readValue("dirty_data") |
| |
| wrd := par.getWritebackRateDebug() |
| bds.WritebackRateDebug = wrd |
| |
| // dir <uuidPath>/<bds.Name>/stats_five_minute |
| par.setSubDir(bds.Name, "stats_five_minute") |
| bds.FiveMin.Bypassed = par.readValue("bypassed") |
| bds.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits") |
| bds.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses") |
| bds.FiveMin.CacheHits = par.readValue("cache_hits") |
| bds.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions") |
| bds.FiveMin.CacheMisses = par.readValue("cache_misses") |
| bds.FiveMin.CacheReadaheads = par.readValue("cache_readaheads") |
| |
| // dir <uuidPath>/<bds.Name>/stats_total |
| par.setSubDir(bds.Name, "stats_total") |
| bds.Total.Bypassed = par.readValue("bypassed") |
| bds.Total.CacheBypassHits = par.readValue("cache_bypass_hits") |
| bds.Total.CacheBypassMisses = par.readValue("cache_bypass_misses") |
| bds.Total.CacheHits = par.readValue("cache_hits") |
| bds.Total.CacheMissCollisions = par.readValue("cache_miss_collisions") |
| bds.Total.CacheMisses = par.readValue("cache_misses") |
| bds.Total.CacheReadaheads = par.readValue("cache_readaheads") |
| } |
| |
| if par.err != nil { |
| return nil, par.err |
| } |
| |
| // cache stats |
| |
| reg = path.Join(uuidPath, "cache[0-9]*") |
| cacheDirs, err := filepath.Glob(reg) |
| if err != nil { |
| return nil, err |
| } |
| bs.Caches = make([]CacheStats, len(cacheDirs)) |
| |
| for ii, cacheDir := range cacheDirs { |
| var cs = &bs.Caches[ii] |
| cs.Name = filepath.Base(cacheDir) |
| |
| // dir is <uuidPath>/<cs.Name> |
| par.setSubDir(cs.Name) |
| cs.IOErrors = par.readValue("io_errors") |
| cs.MetadataWritten = par.readValue("metadata_written") |
| cs.Written = par.readValue("written") |
| |
| if priorityStats { |
| ps := par.getPriorityStats() |
| cs.Priority = ps |
| } |
| } |
| |
| if par.err != nil { |
| return nil, par.err |
| } |
| |
| return &bs, nil |
| } |