blob: 7560a11c5c8734784e1794689ae88259dc35c525 [file] [log] [blame]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03001import os
2from collections import namedtuple
3
4
5SensorInfo = namedtuple("SensorInfo", ['value', 'is_accumulated'])
6
7
8def provides(name: str):
9 def closure(func):
10 return func
11 return closure
12
13
14def 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
28def 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
49def 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
64def 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
106io_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")
119def 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
138def 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
179net_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")
188def 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")
211def 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
229def 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)
250def 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")
283def 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
308def 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
324io_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")
333def 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
366ram_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")
380def 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