1
2
3
4
5
6
7
8
9
10
11
12
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
29
30 type FS struct {
31 sys *fs.FS
32 }
33
34
35
36 func NewDefaultFS() (FS, error) {
37 return NewFS(fs.DefaultSysMountPoint)
38 }
39
40
41
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
54
55 func (fs FS) Stats() ([]*Stats, error) {
56 return fs.stats(true)
57 }
58
59
60
61 func (fs FS) StatsWithoutPriority() ([]*Stats, error) {
62 return fs.stats(false)
63 }
64
65
66
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
76 name := filepath.Base(uuidPath)
77
78
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
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
102 return intPart, nil
103 }
104 fracPart, err := strconv.ParseFloat(ss[1], 64)
105 if err != nil {
106 return 0, err
107 }
108
109
110
111
112
113
114 fracPart = fracPart / 10.24
115 return intPart + fracPart, nil
116 }
117
118
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
131
132 if lastByte > 57 {
133
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
150
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
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
200
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
214 byt = byt[:len(byt)-1]
215 res, err := dehumanize(byt)
216 p.err = err
217 return res
218 }
219
220
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
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
376 func GetStats(uuidPath string, priorityStats bool) (*Stats, error) {
377 var bs Stats
378
379 par := parser{uuidPath: uuidPath}
380
381
382
383
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
393
394
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
402
403
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
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
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
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
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
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
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