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
177type parser struct {
178	uuidPath   string
179	subDir     string
180	currentDir string
181	err        error
182}
183
184func (p *parser) setSubDir(pathElements ...string) {
185	p.subDir = path.Join(pathElements...)
186	p.currentDir = path.Join(p.uuidPath, p.subDir)
187}
188
189func (p *parser) readValue(fileName string) uint64 {
190	if p.err != nil {
191		return 0
192	}
193	path := path.Join(p.currentDir, fileName)
194	byt, err := ioutil.ReadFile(path)
195	if err != nil {
196		p.err = fmt.Errorf("failed to read: %s", path)
197		return 0
198	}
199	// Remove trailing newline.
200	byt = byt[:len(byt)-1]
201	res, err := dehumanize(byt)
202	p.err = err
203	return res
204}
205
206// ParsePriorityStats parses lines from the priority_stats file.
207func parsePriorityStats(line string, ps *PriorityStats) error {
208	var (
209		value uint64
210		err   error
211	)
212	switch {
213	case strings.HasPrefix(line, "Unused:"):
214		fields := strings.Fields(line)
215		rawValue := fields[len(fields)-1]
216		valueStr := strings.TrimSuffix(rawValue, "%")
217		value, err = strconv.ParseUint(valueStr, 10, 64)
218		if err != nil {
219			return err
220		}
221		ps.UnusedPercent = value
222	case strings.HasPrefix(line, "Metadata:"):
223		fields := strings.Fields(line)
224		rawValue := fields[len(fields)-1]
225		valueStr := strings.TrimSuffix(rawValue, "%")
226		value, err = strconv.ParseUint(valueStr, 10, 64)
227		if err != nil {
228			return err
229		}
230		ps.MetadataPercent = value
231	}
232	return nil
233}
234
235func (p *parser) getPriorityStats() PriorityStats {
236	var res PriorityStats
237
238	if p.err != nil {
239		return res
240	}
241
242	path := path.Join(p.currentDir, "priority_stats")
243
244	file, err := os.Open(path)
245	if err != nil {
246		p.err = fmt.Errorf("failed to read: %s", path)
247		return res
248	}
249	defer file.Close()
250
251	scanner := bufio.NewScanner(file)
252	for scanner.Scan() {
253		err = parsePriorityStats(scanner.Text(), &res)
254		if err != nil {
255			p.err = fmt.Errorf("failed to parse: %s (%s)", path, err)
256			return res
257		}
258	}
259	if err := scanner.Err(); err != nil {
260		p.err = fmt.Errorf("failed to parse: %s (%s)", path, err)
261		return res
262	}
263	return res
264}
265
266// GetStats collects from sysfs files data tied to one bcache ID.
267func GetStats(uuidPath string, priorityStats bool) (*Stats, error) {
268	var bs Stats
269
270	par := parser{uuidPath: uuidPath}
271
272	// bcache stats
273
274	// dir <uuidPath>
275	par.setSubDir("")
276	bs.Bcache.AverageKeySize = par.readValue("average_key_size")
277	bs.Bcache.BtreeCacheSize = par.readValue("btree_cache_size")
278	bs.Bcache.CacheAvailablePercent = par.readValue("cache_available_percent")
279	bs.Bcache.Congested = par.readValue("congested")
280	bs.Bcache.RootUsagePercent = par.readValue("root_usage_percent")
281	bs.Bcache.TreeDepth = par.readValue("tree_depth")
282
283	// bcache stats (internal)
284
285	// dir <uuidPath>/internal
286	par.setSubDir("internal")
287	bs.Bcache.Internal.ActiveJournalEntries = par.readValue("active_journal_entries")
288	bs.Bcache.Internal.BtreeNodes = par.readValue("btree_nodes")
289	bs.Bcache.Internal.BtreeReadAverageDurationNanoSeconds = par.readValue("btree_read_average_duration_us")
290	bs.Bcache.Internal.CacheReadRaces = par.readValue("cache_read_races")
291
292	// bcache stats (period)
293
294	// dir <uuidPath>/stats_five_minute
295	par.setSubDir("stats_five_minute")
296	bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed")
297	bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits")
298
299	bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed")
300	bs.Bcache.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits")
301	bs.Bcache.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses")
302	bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits")
303	bs.Bcache.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions")
304	bs.Bcache.FiveMin.CacheMisses = par.readValue("cache_misses")
305	bs.Bcache.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
306
307	// dir <uuidPath>/stats_total
308	par.setSubDir("stats_total")
309	bs.Bcache.Total.Bypassed = par.readValue("bypassed")
310	bs.Bcache.Total.CacheHits = par.readValue("cache_hits")
311
312	bs.Bcache.Total.Bypassed = par.readValue("bypassed")
313	bs.Bcache.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
314	bs.Bcache.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
315	bs.Bcache.Total.CacheHits = par.readValue("cache_hits")
316	bs.Bcache.Total.CacheMissCollisions = par.readValue("cache_miss_collisions")
317	bs.Bcache.Total.CacheMisses = par.readValue("cache_misses")
318	bs.Bcache.Total.CacheReadaheads = par.readValue("cache_readaheads")
319
320	if par.err != nil {
321		return nil, par.err
322	}
323
324	// bdev stats
325
326	reg := path.Join(uuidPath, "bdev[0-9]*")
327	bdevDirs, err := filepath.Glob(reg)
328	if err != nil {
329		return nil, err
330	}
331
332	bs.Bdevs = make([]BdevStats, len(bdevDirs))
333
334	for ii, bdevDir := range bdevDirs {
335		var bds = &bs.Bdevs[ii]
336
337		bds.Name = filepath.Base(bdevDir)
338
339		par.setSubDir(bds.Name)
340		bds.DirtyData = par.readValue("dirty_data")
341
342		// dir <uuidPath>/<bds.Name>/stats_five_minute
343		par.setSubDir(bds.Name, "stats_five_minute")
344		bds.FiveMin.Bypassed = par.readValue("bypassed")
345		bds.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits")
346		bds.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses")
347		bds.FiveMin.CacheHits = par.readValue("cache_hits")
348		bds.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions")
349		bds.FiveMin.CacheMisses = par.readValue("cache_misses")
350		bds.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
351
352		// dir <uuidPath>/<bds.Name>/stats_total
353		par.setSubDir("stats_total")
354		bds.Total.Bypassed = par.readValue("bypassed")
355		bds.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
356		bds.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
357		bds.Total.CacheHits = par.readValue("cache_hits")
358		bds.Total.CacheMissCollisions = par.readValue("cache_miss_collisions")
359		bds.Total.CacheMisses = par.readValue("cache_misses")
360		bds.Total.CacheReadaheads = par.readValue("cache_readaheads")
361	}
362
363	if par.err != nil {
364		return nil, par.err
365	}
366
367	// cache stats
368
369	reg = path.Join(uuidPath, "cache[0-9]*")
370	cacheDirs, err := filepath.Glob(reg)
371	if err != nil {
372		return nil, err
373	}
374	bs.Caches = make([]CacheStats, len(cacheDirs))
375
376	for ii, cacheDir := range cacheDirs {
377		var cs = &bs.Caches[ii]
378		cs.Name = filepath.Base(cacheDir)
379
380		// dir is <uuidPath>/<cs.Name>
381		par.setSubDir(cs.Name)
382		cs.IOErrors = par.readValue("io_errors")
383		cs.MetadataWritten = par.readValue("metadata_written")
384		cs.Written = par.readValue("written")
385
386		if priorityStats {
387			ps := par.getPriorityStats()
388			cs.Priority = ps
389		}
390	}
391
392	if par.err != nil {
393		return nil, par.err
394	}
395
396	return &bs, nil
397}
398