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