...

Source file src/github.com/prometheus/procfs/bcache/get.go

Documentation: github.com/prometheus/procfs/bcache

     1  // Copyright 2017 The Prometheus Authors
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package bcache
    15  
    16  import (
    17  	"bufio"
    18  	"fmt"
    19  	"os"
    20  	"path"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/prometheus/procfs/internal/fs"
    26  )
    27  
    28  // FS represents the pseudo-filesystem proc, which provides an interface to
    29  // kernel data structures.
    30  type FS struct {
    31  	sys *fs.FS
    32  }
    33  
    34  // NewDefaultFS returns a new Bcache using the default sys fs mount point. It will error
    35  // if the mount point can't be read.
    36  func NewDefaultFS() (FS, error) {
    37  	return NewFS(fs.DefaultSysMountPoint)
    38  }
    39  
    40  // NewFS returns a new Bcache using the given sys fs mount point. It will error
    41  // if the mount point can't be read.
    42  func NewFS(mountPoint string) (FS, error) {
    43  	if strings.TrimSpace(mountPoint) == "" {
    44  		mountPoint = fs.DefaultSysMountPoint
    45  	}
    46  	fs, err := fs.NewFS(mountPoint)
    47  	if err != nil {
    48  		return FS{}, err
    49  	}
    50  	return FS{&fs}, nil
    51  }
    52  
    53  // Stats is a wrapper around stats().
    54  // It returns full available statistics.
    55  func (fs FS) Stats() ([]*Stats, error) {
    56  	return fs.stats(true)
    57  }
    58  
    59  // StatsWithoutPriority is a wrapper around stats().
    60  // It ignores priority_stats file, because it is expensive to read.
    61  func (fs FS) StatsWithoutPriority() ([]*Stats, error) {
    62  	return fs.stats(false)
    63  }
    64  
    65  // stats() retrieves bcache runtime statistics for each bcache.
    66  // priorityStats flag controls if we need to read priority_stats.
    67  func (fs FS) stats(priorityStats bool) ([]*Stats, error) {
    68  	matches, err := filepath.Glob(fs.sys.Path("fs/bcache/*-*"))
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	stats := make([]*Stats, 0, len(matches))
    74  	for _, uuidPath := range matches {
    75  		// "*-*" in glob above indicates the name of the bcache.
    76  		name := filepath.Base(uuidPath)
    77  
    78  		// stats
    79  		s, err := GetStats(uuidPath, priorityStats)
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  
    84  		s.Name = name
    85  		stats = append(stats, s)
    86  	}
    87  
    88  	return stats, nil
    89  }
    90  
    91  // ParsePseudoFloat parses the peculiar format produced by bcache's bch_hprint.
    92  func parsePseudoFloat(str string) (float64, error) {
    93  	ss := strings.Split(str, ".")
    94  
    95  	intPart, err := strconv.ParseFloat(ss[0], 64)
    96  	if err != nil {
    97  		return 0, err
    98  	}
    99  
   100  	if len(ss) == 1 {
   101  		// Pure integers are fine.
   102  		return intPart, nil
   103  	}
   104  	fracPart, err := strconv.ParseFloat(ss[1], 64)
   105  	if err != nil {
   106  		return 0, err
   107  	}
   108  	// fracPart is a number between 0 and 1023 divided by 100; it is off
   109  	// by a small amount. Unexpected bumps in time lines may occur because
   110  	// for bch_hprint .1 != .10 and .10 > .9 (at least up to Linux
   111  	// v4.12-rc3).
   112  
   113  	// Restore the proper order:
   114  	fracPart = fracPart / 10.24
   115  	return intPart + fracPart, nil
   116  }
   117  
   118  // Dehumanize converts a human-readable byte slice into a uint64.
   119  func dehumanize(hbytes []byte) (uint64, error) {
   120  	ll := len(hbytes)
   121  	if ll == 0 {
   122  		return 0, fmt.Errorf("zero-length reply")
   123  	}
   124  	lastByte := hbytes[ll-1]
   125  	mul := float64(1)
   126  	var (
   127  		mant float64
   128  		err  error
   129  	)
   130  	// If lastByte is beyond the range of ASCII digits, it must be a
   131  	// multiplier.
   132  	if lastByte > 57 {
   133  		// Remove multiplier from slice.
   134  		hbytes = hbytes[:len(hbytes)-1]
   135  
   136  		const (
   137  			_ = 1 << (10 * iota)
   138  			KiB
   139  			MiB
   140  			GiB
   141  			TiB
   142  			PiB
   143  			EiB
   144  			ZiB
   145  			YiB
   146  		)
   147  
   148  		multipliers := map[rune]float64{
   149  			// Source for conversion rules:
   150  			// linux-kernel/drivers/md/bcache/util.c:bch_hprint()
   151  			'k': KiB,
   152  			'M': MiB,
   153  			'G': GiB,
   154  			'T': TiB,
   155  			'P': PiB,
   156  			'E': EiB,
   157  			'Z': ZiB,
   158  			'Y': YiB,
   159  		}
   160  		mul = multipliers[rune(lastByte)]
   161  		mant, err = parsePseudoFloat(string(hbytes))
   162  		if err != nil {
   163  			return 0, err
   164  		}
   165  	} else {
   166  		// Not humanized by bch_hprint
   167  		mant, err = strconv.ParseFloat(string(hbytes), 64)
   168  		if err != nil {
   169  			return 0, err
   170  		}
   171  	}
   172  	res := uint64(mant * mul)
   173  	return res, nil
   174  }
   175  
   176  func dehumanizeSigned(str string) (int64, error) {
   177  	value, err := dehumanize([]byte(strings.TrimPrefix(str, "-")))
   178  	if err != nil {
   179  		return 0, err
   180  	}
   181  	if strings.HasPrefix(str, "-") {
   182  		return int64(-value), nil
   183  	}
   184  	return int64(value), nil
   185  }
   186  
   187  type parser struct {
   188  	uuidPath   string
   189  	subDir     string
   190  	currentDir string
   191  	err        error
   192  }
   193  
   194  func (p *parser) setSubDir(pathElements ...string) {
   195  	p.subDir = path.Join(pathElements...)
   196  	p.currentDir = path.Join(p.uuidPath, p.subDir)
   197  }
   198  
   199  // readValues reads a number of numerical values into an uint64 slice.
   200  // Non-existing files are ignored.
   201  func (p *parser) readValue(fileName string) uint64 {
   202  	if p.err != nil {
   203  		return 0
   204  	}
   205  	path := path.Join(p.currentDir, fileName)
   206  	byt, err := os.ReadFile(path)
   207  	if err != nil {
   208  		if !os.IsNotExist(err) {
   209  			p.err = fmt.Errorf("failed to read: %s", path)
   210  		}
   211  		return 0
   212  	}
   213  	// Remove trailing newline.
   214  	byt = byt[:len(byt)-1]
   215  	res, err := dehumanize(byt)
   216  	p.err = err
   217  	return res
   218  }
   219  
   220  // ParsePriorityStats parses lines from the priority_stats file.
   221  func parsePriorityStats(line string, ps *PriorityStats) error {
   222  	var (
   223  		value uint64
   224  		err   error
   225  	)
   226  	switch {
   227  	case strings.HasPrefix(line, "Unused:"):
   228  		fields := strings.Fields(line)
   229  		rawValue := fields[len(fields)-1]
   230  		valueStr := strings.TrimSuffix(rawValue, "%")
   231  		value, err = strconv.ParseUint(valueStr, 10, 64)
   232  		if err != nil {
   233  			return err
   234  		}
   235  		ps.UnusedPercent = value
   236  	case strings.HasPrefix(line, "Metadata:"):
   237  		fields := strings.Fields(line)
   238  		rawValue := fields[len(fields)-1]
   239  		valueStr := strings.TrimSuffix(rawValue, "%")
   240  		value, err = strconv.ParseUint(valueStr, 10, 64)
   241  		if err != nil {
   242  			return err
   243  		}
   244  		ps.MetadataPercent = value
   245  	}
   246  	return nil
   247  }
   248  
   249  // ParseWritebackRateDebug parses lines from the writeback_rate_debug file.
   250  func parseWritebackRateDebug(line string, wrd *WritebackRateDebugStats) error {
   251  	switch {
   252  	case strings.HasPrefix(line, "rate:"):
   253  		fields := strings.Fields(line)
   254  		rawValue := fields[len(fields)-1]
   255  		valueStr := strings.TrimSuffix(rawValue, "/sec")
   256  		value, err := dehumanize([]byte(valueStr))
   257  		if err != nil {
   258  			return err
   259  		}
   260  		wrd.Rate = value
   261  	case strings.HasPrefix(line, "dirty:"):
   262  		fields := strings.Fields(line)
   263  		valueStr := fields[len(fields)-1]
   264  		value, err := dehumanize([]byte(valueStr))
   265  		if err != nil {
   266  			return err
   267  		}
   268  		wrd.Dirty = value
   269  	case strings.HasPrefix(line, "target:"):
   270  		fields := strings.Fields(line)
   271  		valueStr := fields[len(fields)-1]
   272  		value, err := dehumanize([]byte(valueStr))
   273  		if err != nil {
   274  			return err
   275  		}
   276  		wrd.Target = value
   277  	case strings.HasPrefix(line, "proportional:"):
   278  		fields := strings.Fields(line)
   279  		valueStr := fields[len(fields)-1]
   280  		value, err := dehumanizeSigned(valueStr)
   281  		if err != nil {
   282  			return err
   283  		}
   284  		wrd.Proportional = value
   285  	case strings.HasPrefix(line, "integral:"):
   286  		fields := strings.Fields(line)
   287  		valueStr := fields[len(fields)-1]
   288  		value, err := dehumanizeSigned(valueStr)
   289  		if err != nil {
   290  			return err
   291  		}
   292  		wrd.Integral = value
   293  	case strings.HasPrefix(line, "change:"):
   294  		fields := strings.Fields(line)
   295  		rawValue := fields[len(fields)-1]
   296  		valueStr := strings.TrimSuffix(rawValue, "/sec")
   297  		value, err := dehumanizeSigned(valueStr)
   298  		if err != nil {
   299  			return err
   300  		}
   301  		wrd.Change = value
   302  	case strings.HasPrefix(line, "next io:"):
   303  		fields := strings.Fields(line)
   304  		rawValue := fields[len(fields)-1]
   305  		valueStr := strings.TrimSuffix(rawValue, "ms")
   306  		value, err := strconv.ParseInt(valueStr, 10, 64)
   307  		if err != nil {
   308  			return err
   309  		}
   310  		wrd.NextIO = value
   311  	}
   312  	return nil
   313  }
   314  
   315  func (p *parser) getPriorityStats() PriorityStats {
   316  	var res PriorityStats
   317  
   318  	if p.err != nil {
   319  		return res
   320  	}
   321  
   322  	path := path.Join(p.currentDir, "priority_stats")
   323  
   324  	file, err := os.Open(path)
   325  	if err != nil {
   326  		p.err = fmt.Errorf("failed to read: %s", path)
   327  		return res
   328  	}
   329  	defer file.Close()
   330  
   331  	scanner := bufio.NewScanner(file)
   332  	for scanner.Scan() {
   333  		err = parsePriorityStats(scanner.Text(), &res)
   334  		if err != nil {
   335  			p.err = fmt.Errorf("failed to parse path %q: %w", path, err)
   336  			return res
   337  		}
   338  	}
   339  	if err := scanner.Err(); err != nil {
   340  		p.err = fmt.Errorf("failed to parse path %q: %w", path, err)
   341  		return res
   342  	}
   343  	return res
   344  }
   345  
   346  func (p *parser) getWritebackRateDebug() WritebackRateDebugStats {
   347  	var res WritebackRateDebugStats
   348  
   349  	if p.err != nil {
   350  		return res
   351  	}
   352  	path := path.Join(p.currentDir, "writeback_rate_debug")
   353  	file, err := os.Open(path)
   354  	if err != nil {
   355  		p.err = fmt.Errorf("failed to read: %s", path)
   356  		return res
   357  	}
   358  	defer file.Close()
   359  
   360  	scanner := bufio.NewScanner(file)
   361  	for scanner.Scan() {
   362  		err = parseWritebackRateDebug(scanner.Text(), &res)
   363  		if err != nil {
   364  			p.err = fmt.Errorf("failed to parse path %q: %w", path, err)
   365  			return res
   366  		}
   367  	}
   368  	if err := scanner.Err(); err != nil {
   369  		p.err = fmt.Errorf("failed to parse path %q: %w", path, err)
   370  		return res
   371  	}
   372  	return res
   373  }
   374  
   375  // GetStats collects from sysfs files data tied to one bcache ID.
   376  func GetStats(uuidPath string, priorityStats bool) (*Stats, error) {
   377  	var bs Stats
   378  
   379  	par := parser{uuidPath: uuidPath}
   380  
   381  	// bcache stats
   382  
   383  	// dir <uuidPath>
   384  	par.setSubDir("")
   385  	bs.Bcache.AverageKeySize = par.readValue("average_key_size")
   386  	bs.Bcache.BtreeCacheSize = par.readValue("btree_cache_size")
   387  	bs.Bcache.CacheAvailablePercent = par.readValue("cache_available_percent")
   388  	bs.Bcache.Congested = par.readValue("congested")
   389  	bs.Bcache.RootUsagePercent = par.readValue("root_usage_percent")
   390  	bs.Bcache.TreeDepth = par.readValue("tree_depth")
   391  
   392  	// bcache stats (internal)
   393  
   394  	// dir <uuidPath>/internal
   395  	par.setSubDir("internal")
   396  	bs.Bcache.Internal.ActiveJournalEntries = par.readValue("active_journal_entries")
   397  	bs.Bcache.Internal.BtreeNodes = par.readValue("btree_nodes")
   398  	bs.Bcache.Internal.BtreeReadAverageDurationNanoSeconds = par.readValue("btree_read_average_duration_us")
   399  	bs.Bcache.Internal.CacheReadRaces = par.readValue("cache_read_races")
   400  
   401  	// bcache stats (period)
   402  
   403  	// dir <uuidPath>/stats_five_minute
   404  	par.setSubDir("stats_five_minute")
   405  	bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed")
   406  	bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits")
   407  
   408  	bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed")
   409  	bs.Bcache.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits")
   410  	bs.Bcache.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses")
   411  	bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits")
   412  	bs.Bcache.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions")
   413  	bs.Bcache.FiveMin.CacheMisses = par.readValue("cache_misses")
   414  	bs.Bcache.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
   415  
   416  	// dir <uuidPath>/stats_total
   417  	par.setSubDir("stats_total")
   418  	bs.Bcache.Total.Bypassed = par.readValue("bypassed")
   419  	bs.Bcache.Total.CacheHits = par.readValue("cache_hits")
   420  
   421  	bs.Bcache.Total.Bypassed = par.readValue("bypassed")
   422  	bs.Bcache.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
   423  	bs.Bcache.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
   424  	bs.Bcache.Total.CacheHits = par.readValue("cache_hits")
   425  	bs.Bcache.Total.CacheMissCollisions = par.readValue("cache_miss_collisions")
   426  	bs.Bcache.Total.CacheMisses = par.readValue("cache_misses")
   427  	bs.Bcache.Total.CacheReadaheads = par.readValue("cache_readaheads")
   428  
   429  	if par.err != nil {
   430  		return nil, par.err
   431  	}
   432  
   433  	// bdev stats
   434  
   435  	reg := path.Join(uuidPath, "bdev[0-9]*")
   436  	bdevDirs, err := filepath.Glob(reg)
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  
   441  	bs.Bdevs = make([]BdevStats, len(bdevDirs))
   442  
   443  	for ii, bdevDir := range bdevDirs {
   444  		var bds = &bs.Bdevs[ii]
   445  
   446  		bds.Name = filepath.Base(bdevDir)
   447  
   448  		par.setSubDir(bds.Name)
   449  		bds.DirtyData = par.readValue("dirty_data")
   450  
   451  		wrd := par.getWritebackRateDebug()
   452  		bds.WritebackRateDebug = wrd
   453  
   454  		// dir <uuidPath>/<bds.Name>/stats_five_minute
   455  		par.setSubDir(bds.Name, "stats_five_minute")
   456  		bds.FiveMin.Bypassed = par.readValue("bypassed")
   457  		bds.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits")
   458  		bds.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses")
   459  		bds.FiveMin.CacheHits = par.readValue("cache_hits")
   460  		bds.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions")
   461  		bds.FiveMin.CacheMisses = par.readValue("cache_misses")
   462  		bds.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
   463  
   464  		// dir <uuidPath>/<bds.Name>/stats_total
   465  		par.setSubDir(bds.Name, "stats_total")
   466  		bds.Total.Bypassed = par.readValue("bypassed")
   467  		bds.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
   468  		bds.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
   469  		bds.Total.CacheHits = par.readValue("cache_hits")
   470  		bds.Total.CacheMissCollisions = par.readValue("cache_miss_collisions")
   471  		bds.Total.CacheMisses = par.readValue("cache_misses")
   472  		bds.Total.CacheReadaheads = par.readValue("cache_readaheads")
   473  	}
   474  
   475  	if par.err != nil {
   476  		return nil, par.err
   477  	}
   478  
   479  	// cache stats
   480  
   481  	reg = path.Join(uuidPath, "cache[0-9]*")
   482  	cacheDirs, err := filepath.Glob(reg)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  	bs.Caches = make([]CacheStats, len(cacheDirs))
   487  
   488  	for ii, cacheDir := range cacheDirs {
   489  		var cs = &bs.Caches[ii]
   490  		cs.Name = filepath.Base(cacheDir)
   491  
   492  		// dir is <uuidPath>/<cs.Name>
   493  		par.setSubDir(cs.Name)
   494  		cs.IOErrors = par.readValue("io_errors")
   495  		cs.MetadataWritten = par.readValue("metadata_written")
   496  		cs.Written = par.readValue("written")
   497  
   498  		if priorityStats {
   499  			ps := par.getPriorityStats()
   500  			cs.Priority = ps
   501  		}
   502  	}
   503  
   504  	if par.err != nil {
   505  		return nil, par.err
   506  	}
   507  
   508  	return &bs, nil
   509  }
   510  

View as plain text