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
27// ParsePseudoFloat parses the peculiar format produced by bcache's bch_hprint.
28func parsePseudoFloat(str string) (float64, error) {
29	ss := strings.Split(str, ".")
30
31	intPart, err := strconv.ParseFloat(ss[0], 64)
32	if err != nil {
33		return 0, err
34	}
35
36	if len(ss) == 1 {
37		// Pure integers are fine.
38		return intPart, nil
39	}
40	fracPart, err := strconv.ParseFloat(ss[1], 64)
41	if err != nil {
42		return 0, err
43	}
44	// fracPart is a number between 0 and 1023 divided by 100; it is off
45	// by a small amount. Unexpected bumps in time lines may occur because
46	// for bch_hprint .1 != .10 and .10 > .9 (at least up to Linux
47	// v4.12-rc3).
48
49	// Restore the proper order:
50	fracPart = fracPart / 10.24
51	return intPart + fracPart, nil
52}
53
54// Dehumanize converts a human-readable byte slice into a uint64.
55func dehumanize(hbytes []byte) (uint64, error) {
56	ll := len(hbytes)
57	if ll == 0 {
58		return 0, fmt.Errorf("zero-length reply")
59	}
60	lastByte := hbytes[ll-1]
61	mul := float64(1)
62	var (
63		mant float64
64		err  error
65	)
66	// If lastByte is beyond the range of ASCII digits, it must be a
67	// multiplier.
68	if lastByte > 57 {
69		// Remove multiplier from slice.
70		hbytes = hbytes[:len(hbytes)-1]
71
72		const (
73			_ = 1 << (10 * iota)
74			KiB
75			MiB
76			GiB
77			TiB
78			PiB
79			EiB
80			ZiB
81			YiB
82		)
83
84		multipliers := map[rune]float64{
85			// Source for conversion rules:
86			// linux-kernel/drivers/md/bcache/util.c:bch_hprint()
87			'k': KiB,
88			'M': MiB,
89			'G': GiB,
90			'T': TiB,
91			'P': PiB,
92			'E': EiB,
93			'Z': ZiB,
94			'Y': YiB,
95		}
96		mul = multipliers[rune(lastByte)]
97		mant, err = parsePseudoFloat(string(hbytes))
98		if err != nil {
99			return 0, err
100		}
101	} else {
102		// Not humanized by bch_hprint
103		mant, err = strconv.ParseFloat(string(hbytes), 64)
104		if err != nil {
105			return 0, err
106		}
107	}
108	res := uint64(mant * mul)
109	return res, nil
110}
111
112type parser struct {
113	uuidPath   string
114	subDir     string
115	currentDir string
116	err        error
117}
118
119func (p *parser) setSubDir(pathElements ...string) {
120	p.subDir = path.Join(pathElements...)
121	p.currentDir = path.Join(p.uuidPath, p.subDir)
122}
123
124func (p *parser) readValue(fileName string) uint64 {
125	if p.err != nil {
126		return 0
127	}
128	path := path.Join(p.currentDir, fileName)
129	byt, err := ioutil.ReadFile(path)
130	if err != nil {
131		p.err = fmt.Errorf("failed to read: %s", path)
132		return 0
133	}
134	// Remove trailing newline.
135	byt = byt[:len(byt)-1]
136	res, err := dehumanize(byt)
137	p.err = err
138	return res
139}
140
141// ParsePriorityStats parses lines from the priority_stats file.
142func parsePriorityStats(line string, ps *PriorityStats) error {
143	var (
144		value uint64
145		err   error
146	)
147	switch {
148	case strings.HasPrefix(line, "Unused:"):
149		fields := strings.Fields(line)
150		rawValue := fields[len(fields)-1]
151		valueStr := strings.TrimSuffix(rawValue, "%")
152		value, err = strconv.ParseUint(valueStr, 10, 64)
153		if err != nil {
154			return err
155		}
156		ps.UnusedPercent = value
157	case strings.HasPrefix(line, "Metadata:"):
158		fields := strings.Fields(line)
159		rawValue := fields[len(fields)-1]
160		valueStr := strings.TrimSuffix(rawValue, "%")
161		value, err = strconv.ParseUint(valueStr, 10, 64)
162		if err != nil {
163			return err
164		}
165		ps.MetadataPercent = value
166	}
167	return nil
168}
169
170func (p *parser) getPriorityStats() PriorityStats {
171	var res PriorityStats
172
173	if p.err != nil {
174		return res
175	}
176
177	path := path.Join(p.currentDir, "priority_stats")
178
179	file, err := os.Open(path)
180	if err != nil {
181		p.err = fmt.Errorf("failed to read: %s", path)
182		return res
183	}
184	defer file.Close()
185
186	scanner := bufio.NewScanner(file)
187	for scanner.Scan() {
188		err = parsePriorityStats(scanner.Text(), &res)
189		if err != nil {
190			p.err = fmt.Errorf("failed to parse: %s (%s)", path, err)
191			return res
192		}
193	}
194	if err := scanner.Err(); err != nil {
195		p.err = fmt.Errorf("failed to parse: %s (%s)", path, err)
196		return res
197	}
198	return res
199}
200
201// GetStats collects from sysfs files data tied to one bcache ID.
202func GetStats(uuidPath string) (*Stats, error) {
203	var bs Stats
204
205	par := parser{uuidPath: uuidPath}
206
207	// bcache stats
208
209	// dir <uuidPath>
210	par.setSubDir("")
211	bs.Bcache.AverageKeySize = par.readValue("average_key_size")
212	bs.Bcache.BtreeCacheSize = par.readValue("btree_cache_size")
213	bs.Bcache.CacheAvailablePercent = par.readValue("cache_available_percent")
214	bs.Bcache.Congested = par.readValue("congested")
215	bs.Bcache.RootUsagePercent = par.readValue("root_usage_percent")
216	bs.Bcache.TreeDepth = par.readValue("tree_depth")
217
218	// bcache stats (internal)
219
220	// dir <uuidPath>/internal
221	par.setSubDir("internal")
222	bs.Bcache.Internal.ActiveJournalEntries = par.readValue("active_journal_entries")
223	bs.Bcache.Internal.BtreeNodes = par.readValue("btree_nodes")
224	bs.Bcache.Internal.BtreeReadAverageDurationNanoSeconds = par.readValue("btree_read_average_duration_us")
225	bs.Bcache.Internal.CacheReadRaces = par.readValue("cache_read_races")
226
227	// bcache stats (period)
228
229	// dir <uuidPath>/stats_five_minute
230	par.setSubDir("stats_five_minute")
231	bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed")
232	bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits")
233
234	bs.Bcache.FiveMin.Bypassed = par.readValue("bypassed")
235	bs.Bcache.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits")
236	bs.Bcache.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses")
237	bs.Bcache.FiveMin.CacheHits = par.readValue("cache_hits")
238	bs.Bcache.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions")
239	bs.Bcache.FiveMin.CacheMisses = par.readValue("cache_misses")
240	bs.Bcache.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
241
242	// dir <uuidPath>/stats_total
243	par.setSubDir("stats_total")
244	bs.Bcache.Total.Bypassed = par.readValue("bypassed")
245	bs.Bcache.Total.CacheHits = par.readValue("cache_hits")
246
247	bs.Bcache.Total.Bypassed = par.readValue("bypassed")
248	bs.Bcache.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
249	bs.Bcache.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
250	bs.Bcache.Total.CacheHits = par.readValue("cache_hits")
251	bs.Bcache.Total.CacheMissCollisions = par.readValue("cache_miss_collisions")
252	bs.Bcache.Total.CacheMisses = par.readValue("cache_misses")
253	bs.Bcache.Total.CacheReadaheads = par.readValue("cache_readaheads")
254
255	if par.err != nil {
256		return nil, par.err
257	}
258
259	// bdev stats
260
261	reg := path.Join(uuidPath, "bdev[0-9]*")
262	bdevDirs, err := filepath.Glob(reg)
263	if err != nil {
264		return nil, err
265	}
266
267	bs.Bdevs = make([]BdevStats, len(bdevDirs))
268
269	for ii, bdevDir := range bdevDirs {
270		var bds = &bs.Bdevs[ii]
271
272		bds.Name = filepath.Base(bdevDir)
273
274		par.setSubDir(bds.Name)
275		bds.DirtyData = par.readValue("dirty_data")
276
277		// dir <uuidPath>/<bds.Name>/stats_five_minute
278		par.setSubDir(bds.Name, "stats_five_minute")
279		bds.FiveMin.Bypassed = par.readValue("bypassed")
280		bds.FiveMin.CacheBypassHits = par.readValue("cache_bypass_hits")
281		bds.FiveMin.CacheBypassMisses = par.readValue("cache_bypass_misses")
282		bds.FiveMin.CacheHits = par.readValue("cache_hits")
283		bds.FiveMin.CacheMissCollisions = par.readValue("cache_miss_collisions")
284		bds.FiveMin.CacheMisses = par.readValue("cache_misses")
285		bds.FiveMin.CacheReadaheads = par.readValue("cache_readaheads")
286
287		// dir <uuidPath>/<bds.Name>/stats_total
288		par.setSubDir("stats_total")
289		bds.Total.Bypassed = par.readValue("bypassed")
290		bds.Total.CacheBypassHits = par.readValue("cache_bypass_hits")
291		bds.Total.CacheBypassMisses = par.readValue("cache_bypass_misses")
292		bds.Total.CacheHits = par.readValue("cache_hits")
293		bds.Total.CacheMissCollisions = par.readValue("cache_miss_collisions")
294		bds.Total.CacheMisses = par.readValue("cache_misses")
295		bds.Total.CacheReadaheads = par.readValue("cache_readaheads")
296	}
297
298	if par.err != nil {
299		return nil, par.err
300	}
301
302	// cache stats
303
304	reg = path.Join(uuidPath, "cache[0-9]*")
305	cacheDirs, err := filepath.Glob(reg)
306	if err != nil {
307		return nil, err
308	}
309	bs.Caches = make([]CacheStats, len(cacheDirs))
310
311	for ii, cacheDir := range cacheDirs {
312		var cs = &bs.Caches[ii]
313		cs.Name = filepath.Base(cacheDir)
314
315		// dir is <uuidPath>/<cs.Name>
316		par.setSubDir(cs.Name)
317		cs.IOErrors = par.readValue("io_errors")
318		cs.MetadataWritten = par.readValue("metadata_written")
319		cs.Written = par.readValue("written")
320
321		ps := par.getPriorityStats()
322		cs.Priority = ps
323	}
324
325	if par.err != nil {
326		return nil, par.err
327	}
328
329	return &bs, nil
330}
331