koder aka kdanilov | 3b4da8b | 2016-10-17 00:17:53 +0300 | [diff] [blame^] | 1 | import os |
| 2 | from collections import namedtuple |
| 3 | |
| 4 | |
| 5 | SensorInfo = namedtuple("SensorInfo", ['value', 'is_accumulated']) |
| 6 | |
| 7 | |
| 8 | def provides(name: str): |
| 9 | def closure(func): |
| 10 | return func |
| 11 | return closure |
| 12 | |
| 13 | |
| 14 | def is_dev_accepted(name, disallowed_prefixes, allowed_prefixes): |
| 15 | dev_ok = True |
| 16 | |
| 17 | if disallowed_prefixes is not None: |
| 18 | dev_ok = all(not name.startswith(prefix) |
| 19 | for prefix in disallowed_prefixes) |
| 20 | |
| 21 | if dev_ok and allowed_prefixes is not None: |
| 22 | dev_ok = any(name.startswith(prefix) |
| 23 | for prefix in allowed_prefixes) |
| 24 | |
| 25 | return dev_ok |
| 26 | |
| 27 | |
| 28 | def get_pid_list(disallowed_prefixes, allowed_prefixes): |
| 29 | """Return pid list from list of pids and names""" |
| 30 | # exceptions |
| 31 | but = disallowed_prefixes if disallowed_prefixes is not None else [] |
| 32 | if allowed_prefixes is None: |
| 33 | # if nothing setted - all ps will be returned except setted |
| 34 | result = [pid |
| 35 | for pid in os.listdir('/proc') |
| 36 | if pid.isdigit() and pid not in but] |
| 37 | else: |
| 38 | result = [] |
| 39 | for pid in os.listdir('/proc'): |
| 40 | if pid.isdigit() and pid not in but: |
| 41 | name = get_pid_name(pid) |
| 42 | if pid in allowed_prefixes or \ |
| 43 | any(name.startswith(val) for val in allowed_prefixes): |
| 44 | # this is allowed pid? |
| 45 | result.append(pid) |
| 46 | return result |
| 47 | |
| 48 | |
| 49 | def get_pid_name(pid): |
| 50 | """Return name by pid""" |
| 51 | try: |
| 52 | with open(os.path.join('/proc/', pid, 'cmdline'), 'r') as pidfile: |
| 53 | try: |
| 54 | cmd = pidfile.readline().split()[0] |
| 55 | return os.path.basename(cmd).rstrip('\x00') |
| 56 | except IndexError: |
| 57 | # no cmd returned |
| 58 | return "<NO NAME>" |
| 59 | except IOError: |
| 60 | # upstream wait any string, no matter if we couldn't read proc |
| 61 | return "no_such_process" |
| 62 | |
| 63 | |
| 64 | def delta(func, only_upd=True): |
| 65 | prev = {} |
| 66 | while True: |
| 67 | for dev_name, vals in func(): |
| 68 | if dev_name not in prev: |
| 69 | prev[dev_name] = {} |
| 70 | for name, (val, _) in vals.items(): |
| 71 | prev[dev_name][name] = val |
| 72 | else: |
| 73 | dev_prev = prev[dev_name] |
| 74 | res = {} |
| 75 | for stat_name, (val, accum_val) in vals.items(): |
| 76 | if accum_val: |
| 77 | if stat_name in dev_prev: |
| 78 | delta = int(val) - int(dev_prev[stat_name]) |
| 79 | if not only_upd or 0 != delta: |
| 80 | res[stat_name] = str(delta) |
| 81 | dev_prev[stat_name] = val |
| 82 | elif not only_upd or '0' != val: |
| 83 | res[stat_name] = val |
| 84 | |
| 85 | if only_upd and len(res) == 0: |
| 86 | continue |
| 87 | yield dev_name, res |
| 88 | yield None, None |
| 89 | |
| 90 | |
| 91 | # 1 - major number |
| 92 | # 2 - minor mumber |
| 93 | # 3 - device name |
| 94 | # 4 - reads completed successfully |
| 95 | # 5 - reads merged |
| 96 | # 6 - sectors read |
| 97 | # 7 - time spent reading (ms) |
| 98 | # 8 - writes completed |
| 99 | # 9 - writes merged |
| 100 | # 10 - sectors written |
| 101 | # 11 - time spent writing (ms) |
| 102 | # 12 - I/Os currently in progress |
| 103 | # 13 - time spent doing I/Os (ms) |
| 104 | # 14 - weighted time spent doing I/Os (ms) |
| 105 | |
| 106 | io_values_pos = [ |
| 107 | (3, 'reads_completed', True), |
| 108 | (5, 'sectors_read', True), |
| 109 | (6, 'rtime', True), |
| 110 | (7, 'writes_completed', True), |
| 111 | (9, 'sectors_written', True), |
| 112 | (10, 'wtime', True), |
| 113 | (11, 'io_queue', False), |
| 114 | (13, 'io_time', True) |
| 115 | ] |
| 116 | |
| 117 | |
| 118 | @provides("block-io") |
| 119 | def io_stat(disallowed_prefixes=('ram', 'loop'), allowed_prefixes=None): |
| 120 | results = {} |
| 121 | for line in open('/proc/diskstats'): |
| 122 | vals = line.split() |
| 123 | dev_name = vals[2] |
| 124 | |
| 125 | dev_ok = is_dev_accepted(dev_name, |
| 126 | disallowed_prefixes, |
| 127 | allowed_prefixes) |
| 128 | if dev_name[-1].isdigit(): |
| 129 | dev_ok = False |
| 130 | |
| 131 | if dev_ok: |
| 132 | for pos, name, accum_val in io_values_pos: |
| 133 | sensor_name = "{0}.{1}".format(dev_name, name) |
| 134 | results[sensor_name] = SensorInfo(int(vals[pos]), accum_val) |
| 135 | return results |
| 136 | |
| 137 | |
| 138 | def get_latency(stat1, stat2): |
| 139 | disks = set(i.split('.', 1)[0] for i in stat1) |
| 140 | results = {} |
| 141 | |
| 142 | for disk in disks: |
| 143 | rdc = disk + '.reads_completed' |
| 144 | wrc = disk + '.writes_completed' |
| 145 | rdt = disk + '.rtime' |
| 146 | wrt = disk + '.wtime' |
| 147 | lat = 0.0 |
| 148 | |
| 149 | io_ops1 = stat1[rdc].value + stat1[wrc].value |
| 150 | io_ops2 = stat2[rdc].value + stat2[wrc].value |
| 151 | |
| 152 | diops = io_ops2 - io_ops1 |
| 153 | |
| 154 | if diops != 0: |
| 155 | io1 = stat1[rdt].value + stat1[wrt].value |
| 156 | io2 = stat2[rdt].value + stat2[wrt].value |
| 157 | lat = abs(float(io1 - io2)) / diops |
| 158 | |
| 159 | results[disk + '.latence'] = SensorInfo(lat, False) |
| 160 | |
| 161 | return results |
| 162 | |
| 163 | |
| 164 | # 1 - major number |
| 165 | # 2 - minor mumber |
| 166 | # 3 - device name |
| 167 | # 4 - reads completed successfully |
| 168 | # 5 - reads merged |
| 169 | # 6 - sectors read |
| 170 | # 7 - time spent reading (ms) |
| 171 | # 8 - writes completed |
| 172 | # 9 - writes merged |
| 173 | # 10 - sectors written |
| 174 | # 11 - time spent writing (ms) |
| 175 | # 12 - I/Os currently in progress |
| 176 | # 13 - time spent doing I/Os (ms) |
| 177 | # 14 - weighted time spent doing I/Os (ms) |
| 178 | |
| 179 | net_values_pos = [ |
| 180 | (0, 'recv_bytes', True), |
| 181 | (1, 'recv_packets', True), |
| 182 | (8, 'send_bytes', True), |
| 183 | (9, 'send_packets', True), |
| 184 | ] |
| 185 | |
| 186 | |
| 187 | @provides("net-io") |
| 188 | def net_stat(disallowed_prefixes=('docker', 'lo'), allowed_prefixes=('eth',)): |
| 189 | results = {} |
| 190 | |
| 191 | for line in open('/proc/net/dev').readlines()[2:]: |
| 192 | dev_name, stats = line.split(":", 1) |
| 193 | dev_name = dev_name.strip() |
| 194 | vals = stats.split() |
| 195 | |
| 196 | dev_ok = is_dev_accepted(dev_name, |
| 197 | disallowed_prefixes, |
| 198 | allowed_prefixes) |
| 199 | |
| 200 | if '.' in dev_name and dev_name.split('.')[-1].isdigit(): |
| 201 | dev_ok = False |
| 202 | |
| 203 | if dev_ok: |
| 204 | for pos, name, accum_val in net_values_pos: |
| 205 | sensor_name = "{0}.{1}".format(dev_name, name) |
| 206 | results[sensor_name] = SensorInfo(int(vals[pos]), accum_val) |
| 207 | return results |
| 208 | |
| 209 | |
| 210 | @provides("perprocess-cpu") |
| 211 | def pscpu_stat(disallowed_prefixes=None, allowed_prefixes=None): |
| 212 | results = {} |
| 213 | pid_list = get_pid_list(disallowed_prefixes, allowed_prefixes) |
| 214 | |
| 215 | for pid in pid_list: |
| 216 | try: |
| 217 | dev_name = get_pid_name(pid) |
| 218 | |
| 219 | pid_stat1 = pid_stat(pid) |
| 220 | |
| 221 | sensor_name = "{0}.{1}".format(dev_name, pid) |
| 222 | results[sensor_name] = SensorInfo(pid_stat1, True) |
| 223 | except IOError: |
| 224 | # may be, proc has already terminated, skip it |
| 225 | continue |
| 226 | return results |
| 227 | |
| 228 | |
| 229 | def pid_stat(pid): |
| 230 | """Return total cpu usage time from process""" |
| 231 | # read /proc/pid/stat |
| 232 | with open(os.path.join('/proc/', pid, 'stat'), 'r') as pidfile: |
| 233 | proctimes = pidfile.readline().split() |
| 234 | # get utime from /proc/<pid>/stat, 14 item |
| 235 | utime = proctimes[13] |
| 236 | # get stime from proc/<pid>/stat, 15 item |
| 237 | stime = proctimes[14] |
| 238 | # count total process used time |
| 239 | return float(int(utime) + int(stime)) |
| 240 | |
| 241 | |
| 242 | # Based on ps_mem.py: |
| 243 | # Licence: LGPLv2 |
| 244 | # Author: P@draigBrady.com |
| 245 | # Source: http://www.pixelbeat.org/scripts/ps_mem.py |
| 246 | # http://github.com/pixelb/scripts/commits/master/scripts/ps_mem.py |
| 247 | |
| 248 | |
| 249 | # Note shared is always a subset of rss (trs is not always) |
| 250 | def get_mem_stats(pid): |
| 251 | """Return memory data of pid in format (private, shared)""" |
| 252 | |
| 253 | fname = '/proc/{0}/{1}'.format(pid, "smaps") |
| 254 | lines = open(fname).readlines() |
| 255 | |
| 256 | shared = 0 |
| 257 | private = 0 |
| 258 | pss = 0 |
| 259 | |
| 260 | # add 0.5KiB as this avg error due to trunctation |
| 261 | pss_adjust = 0.5 |
| 262 | |
| 263 | for line in lines: |
| 264 | if line.startswith("Shared"): |
| 265 | shared += int(line.split()[1]) |
| 266 | |
| 267 | if line.startswith("Private"): |
| 268 | private += int(line.split()[1]) |
| 269 | |
| 270 | if line.startswith("Pss"): |
| 271 | pss += float(line.split()[1]) + pss_adjust |
| 272 | |
| 273 | # Note Shared + Private = Rss above |
| 274 | # The Rss in smaps includes video card mem etc. |
| 275 | |
| 276 | if pss != 0: |
| 277 | shared = int(pss - private) |
| 278 | |
| 279 | return (private, shared) |
| 280 | |
| 281 | |
| 282 | @provides("perprocess-ram") |
| 283 | def psram_stat(disallowed_prefixes=None, allowed_prefixes=None): |
| 284 | results = {} |
| 285 | pid_list = get_pid_list(disallowed_prefixes, allowed_prefixes) |
| 286 | for pid in pid_list: |
| 287 | try: |
| 288 | dev_name = get_pid_name(pid) |
| 289 | |
| 290 | private, shared = get_mem_stats(pid) |
| 291 | total = private + shared |
| 292 | sys_total = get_ram_size() |
| 293 | usage = float(total) / float(sys_total) |
| 294 | |
| 295 | sensor_name = "{0}({1})".format(dev_name, pid) |
| 296 | |
| 297 | results[sensor_name + ".private_mem"] = SensorInfo(private, False) |
| 298 | results[sensor_name + ".shared_mem"] = SensorInfo(shared, False) |
| 299 | results[sensor_name + ".used_mem"] = SensorInfo(total, False) |
| 300 | name = sensor_name + ".mem_usage_percent" |
| 301 | results[name] = SensorInfo(usage * 100, False) |
| 302 | except IOError: |
| 303 | # permission denied or proc die |
| 304 | continue |
| 305 | return results |
| 306 | |
| 307 | |
| 308 | def get_ram_size(): |
| 309 | """Return RAM size in Kb""" |
| 310 | with open("/proc/meminfo") as proc: |
| 311 | mem_total = proc.readline().split() |
| 312 | return mem_total[1] |
| 313 | |
| 314 | |
| 315 | # 0 - cpu name |
| 316 | # 1 - user: normal processes executing in user mode |
| 317 | # 2 - nice: niced processes executing in user mode |
| 318 | # 3 - system: processes executing in kernel mode |
| 319 | # 4 - idle: twiddling thumbs |
| 320 | # 5 - iowait: waiting for I/O to complete |
| 321 | # 6 - irq: servicing interrupts |
| 322 | # 7 - softirq: servicing softirqs |
| 323 | |
| 324 | io_values_pos = [ |
| 325 | (1, 'user_processes', True), |
| 326 | (2, 'nice_processes', True), |
| 327 | (3, 'system_processes', True), |
| 328 | (4, 'idle_time', True), |
| 329 | ] |
| 330 | |
| 331 | |
| 332 | @provides("system-cpu") |
| 333 | def syscpu_stat(disallowed_prefixes=None, allowed_prefixes=None): |
| 334 | results = {} |
| 335 | |
| 336 | # calculate core count |
| 337 | core_count = 0 |
| 338 | |
| 339 | for line in open('/proc/stat'): |
| 340 | vals = line.split() |
| 341 | dev_name = vals[0] |
| 342 | |
| 343 | if dev_name == 'cpu': |
| 344 | for pos, name, accum_val in io_values_pos: |
| 345 | sensor_name = "{0}.{1}".format(dev_name, name) |
| 346 | results[sensor_name] = SensorInfo(int(vals[pos]), |
| 347 | accum_val) |
| 348 | elif dev_name == 'procs_blocked': |
| 349 | val = int(vals[1]) |
| 350 | results["cpu.procs_blocked"] = SensorInfo(val, False) |
| 351 | elif dev_name.startswith('cpu'): |
| 352 | core_count += 1 |
| 353 | |
| 354 | # procs in queue |
| 355 | TASKSPOS = 3 |
| 356 | vals = open('/proc/loadavg').read().split() |
| 357 | ready_procs = vals[TASKSPOS].partition('/')[0] |
| 358 | # dec on current proc |
| 359 | procs_queue = (float(ready_procs) - 1) / core_count |
| 360 | results["cpu.procs_queue"] = SensorInfo(procs_queue, False) |
| 361 | |
| 362 | return results |
| 363 | |
| 364 | |
| 365 | # return this values or setted in allowed |
| 366 | ram_fields = [ |
| 367 | 'MemTotal', |
| 368 | 'MemFree', |
| 369 | 'Buffers', |
| 370 | 'Cached', |
| 371 | 'SwapCached', |
| 372 | 'Dirty', |
| 373 | 'Writeback', |
| 374 | 'SwapTotal', |
| 375 | 'SwapFree' |
| 376 | ] |
| 377 | |
| 378 | |
| 379 | @provides("system-ram") |
| 380 | def sysram_stat(disallowed_prefixes=None, allowed_prefixes=None): |
| 381 | if allowed_prefixes is None: |
| 382 | allowed_prefixes = ram_fields |
| 383 | results = {} |
| 384 | for line in open('/proc/meminfo'): |
| 385 | vals = line.split() |
| 386 | dev_name = vals[0].rstrip(":") |
| 387 | |
| 388 | dev_ok = is_dev_accepted(dev_name, |
| 389 | disallowed_prefixes, |
| 390 | allowed_prefixes) |
| 391 | |
| 392 | title = "ram.{0}".format(dev_name) |
| 393 | |
| 394 | if dev_ok: |
| 395 | results[title] = SensorInfo(int(vals[1]), False) |
| 396 | |
| 397 | if 'ram.MemFree' in results and 'ram.MemTotal' in results: |
| 398 | used = results['ram.MemTotal'].value - results['ram.MemFree'].value |
| 399 | usage = float(used) / results['ram.MemTotal'].value |
| 400 | results["ram.usage_percent"] = SensorInfo(usage, False) |
| 401 | return results |