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