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