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