blob: c218bff18bbe2cecb2e5491c3fbf107318ac47c1 [file] [log] [blame]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03001import os
koder aka kdanilov70227062016-11-26 23:23:21 +02002import time
3import array
4import threading
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03005
6
koder aka kdanilov70227062016-11-26 23:23:21 +02007mod_name = "sensor"
8__version__ = (0, 1)
9
10
11SensorsMap = {}
12
13
14def provides(name):
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030015 def closure(func):
koder aka kdanilov70227062016-11-26 23:23:21 +020016 SensorsMap[name] = func
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030017 return func
18 return closure
19
20
21def is_dev_accepted(name, disallowed_prefixes, allowed_prefixes):
22 dev_ok = True
23
24 if disallowed_prefixes is not None:
25 dev_ok = all(not name.startswith(prefix)
26 for prefix in disallowed_prefixes)
27
28 if dev_ok and allowed_prefixes is not None:
29 dev_ok = any(name.startswith(prefix)
30 for prefix in allowed_prefixes)
31
32 return dev_ok
33
34
35def get_pid_list(disallowed_prefixes, allowed_prefixes):
36 """Return pid list from list of pids and names"""
37 # exceptions
koder aka kdanilov70227062016-11-26 23:23:21 +020038 disallowed = disallowed_prefixes if disallowed_prefixes is not None else []
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030039 if allowed_prefixes is None:
40 # if nothing setted - all ps will be returned except setted
41 result = [pid
42 for pid in os.listdir('/proc')
koder aka kdanilov70227062016-11-26 23:23:21 +020043 if pid.isdigit() and pid not in disallowed]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030044 else:
45 result = []
46 for pid in os.listdir('/proc'):
koder aka kdanilov70227062016-11-26 23:23:21 +020047 if pid.isdigit() and pid not in disallowed:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030048 name = get_pid_name(pid)
49 if pid in allowed_prefixes or \
50 any(name.startswith(val) for val in allowed_prefixes):
51 # this is allowed pid?
52 result.append(pid)
53 return result
54
55
56def get_pid_name(pid):
57 """Return name by pid"""
58 try:
59 with open(os.path.join('/proc/', pid, 'cmdline'), 'r') as pidfile:
60 try:
61 cmd = pidfile.readline().split()[0]
62 return os.path.basename(cmd).rstrip('\x00')
63 except IndexError:
64 # no cmd returned
65 return "<NO NAME>"
66 except IOError:
67 # upstream wait any string, no matter if we couldn't read proc
68 return "no_such_process"
69
70
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030071# 1 - major number
72# 2 - minor mumber
73# 3 - device name
74# 4 - reads completed successfully
75# 5 - reads merged
76# 6 - sectors read
77# 7 - time spent reading (ms)
78# 8 - writes completed
79# 9 - writes merged
80# 10 - sectors written
81# 11 - time spent writing (ms)
82# 12 - I/Os currently in progress
83# 13 - time spent doing I/Os (ms)
84# 14 - weighted time spent doing I/Os (ms)
85
86io_values_pos = [
87 (3, 'reads_completed', True),
88 (5, 'sectors_read', True),
89 (6, 'rtime', True),
90 (7, 'writes_completed', True),
91 (9, 'sectors_written', True),
92 (10, 'wtime', True),
93 (11, 'io_queue', False),
94 (13, 'io_time', True)
95]
96
97
98@provides("block-io")
99def io_stat(disallowed_prefixes=('ram', 'loop'), allowed_prefixes=None):
100 results = {}
101 for line in open('/proc/diskstats'):
102 vals = line.split()
103 dev_name = vals[2]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300104 dev_ok = is_dev_accepted(dev_name,
105 disallowed_prefixes,
106 allowed_prefixes)
koder aka kdanilov70227062016-11-26 23:23:21 +0200107 if not dev_ok or dev_name[-1].isdigit():
108 continue
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300109
koder aka kdanilov70227062016-11-26 23:23:21 +0200110 for pos, name, _ in io_values_pos:
111 results["{0}.{1}".format(dev_name, name)] = int(vals[pos])
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300112 return results
113
114
115# 1 - major number
116# 2 - minor mumber
117# 3 - device name
118# 4 - reads completed successfully
119# 5 - reads merged
120# 6 - sectors read
121# 7 - time spent reading (ms)
122# 8 - writes completed
123# 9 - writes merged
124# 10 - sectors written
125# 11 - time spent writing (ms)
126# 12 - I/Os currently in progress
127# 13 - time spent doing I/Os (ms)
128# 14 - weighted time spent doing I/Os (ms)
129
130net_values_pos = [
131 (0, 'recv_bytes', True),
132 (1, 'recv_packets', True),
133 (8, 'send_bytes', True),
134 (9, 'send_packets', True),
135]
136
137
138@provides("net-io")
139def net_stat(disallowed_prefixes=('docker', 'lo'), allowed_prefixes=('eth',)):
140 results = {}
141
142 for line in open('/proc/net/dev').readlines()[2:]:
143 dev_name, stats = line.split(":", 1)
144 dev_name = dev_name.strip()
145 vals = stats.split()
146
147 dev_ok = is_dev_accepted(dev_name,
148 disallowed_prefixes,
149 allowed_prefixes)
150
151 if '.' in dev_name and dev_name.split('.')[-1].isdigit():
152 dev_ok = False
153
154 if dev_ok:
koder aka kdanilov70227062016-11-26 23:23:21 +0200155 for pos, name, _ in net_values_pos:
156 results["{0}.{1}".format(dev_name, name)] = int(vals[pos])
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300157 return results
158
159
160def pid_stat(pid):
161 """Return total cpu usage time from process"""
162 # read /proc/pid/stat
163 with open(os.path.join('/proc/', pid, 'stat'), 'r') as pidfile:
164 proctimes = pidfile.readline().split()
165 # get utime from /proc/<pid>/stat, 14 item
166 utime = proctimes[13]
167 # get stime from proc/<pid>/stat, 15 item
168 stime = proctimes[14]
169 # count total process used time
170 return float(int(utime) + int(stime))
171
172
koder aka kdanilov70227062016-11-26 23:23:21 +0200173@provides("perprocess-cpu")
174def pscpu_stat(disallowed_prefixes=None, allowed_prefixes=None):
175 results = {}
176 # TODO(koder): fixed list of PID's nust be given
177 for pid in get_pid_list(disallowed_prefixes, allowed_prefixes):
178 try:
179 results["{0}.{1}".format(get_pid_name(pid), pid)] = pid_stat(pid)
180 except IOError:
181 # may be, proc has already terminated, skip it
182 continue
183 return results
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300184
185
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300186def get_mem_stats(pid):
187 """Return memory data of pid in format (private, shared)"""
188
189 fname = '/proc/{0}/{1}'.format(pid, "smaps")
190 lines = open(fname).readlines()
191
192 shared = 0
193 private = 0
194 pss = 0
195
koder aka kdanilov70227062016-11-26 23:23:21 +0200196 # add 0.5KiB as this avg error due to truncation
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300197 pss_adjust = 0.5
198
199 for line in lines:
200 if line.startswith("Shared"):
201 shared += int(line.split()[1])
202
203 if line.startswith("Private"):
204 private += int(line.split()[1])
205
206 if line.startswith("Pss"):
207 pss += float(line.split()[1]) + pss_adjust
208
209 # Note Shared + Private = Rss above
210 # The Rss in smaps includes video card mem etc.
211
212 if pss != 0:
213 shared = int(pss - private)
214
215 return (private, shared)
216
217
koder aka kdanilov70227062016-11-26 23:23:21 +0200218def get_ram_size():
219 """Return RAM size in Kb"""
220 with open("/proc/meminfo") as proc:
221 mem_total = proc.readline().split()
222 return int(mem_total[1])
223
224
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300225@provides("perprocess-ram")
226def psram_stat(disallowed_prefixes=None, allowed_prefixes=None):
227 results = {}
koder aka kdanilov70227062016-11-26 23:23:21 +0200228 # TODO(koder): fixed list of PID's nust be given
229 for pid in get_pid_list(disallowed_prefixes, allowed_prefixes):
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300230 try:
231 dev_name = get_pid_name(pid)
232
233 private, shared = get_mem_stats(pid)
234 total = private + shared
235 sys_total = get_ram_size()
koder aka kdanilov70227062016-11-26 23:23:21 +0200236 usage = float(total) / sys_total
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300237
238 sensor_name = "{0}({1})".format(dev_name, pid)
239
koder aka kdanilov70227062016-11-26 23:23:21 +0200240 results.update([
241 (sensor_name + ".private_mem", private),
242 (sensor_name + ".shared_mem", shared),
243 (sensor_name + ".used_mem", total),
244 (sensor_name + ".mem_usage_percent", int(usage * 100))])
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300245 except IOError:
246 # permission denied or proc die
247 continue
248 return results
249
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300250# 0 - cpu name
251# 1 - user: normal processes executing in user mode
252# 2 - nice: niced processes executing in user mode
253# 3 - system: processes executing in kernel mode
254# 4 - idle: twiddling thumbs
255# 5 - iowait: waiting for I/O to complete
256# 6 - irq: servicing interrupts
257# 7 - softirq: servicing softirqs
258
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200259cpu_values_pos = [
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300260 (1, 'user_processes', True),
261 (2, 'nice_processes', True),
262 (3, 'system_processes', True),
263 (4, 'idle_time', True),
264]
265
266
267@provides("system-cpu")
268def syscpu_stat(disallowed_prefixes=None, allowed_prefixes=None):
269 results = {}
270
271 # calculate core count
272 core_count = 0
273
274 for line in open('/proc/stat'):
275 vals = line.split()
276 dev_name = vals[0]
277
278 if dev_name == 'cpu':
koder aka kdanilov70227062016-11-26 23:23:21 +0200279 for pos, name, _ in cpu_values_pos:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300280 sensor_name = "{0}.{1}".format(dev_name, name)
koder aka kdanilov70227062016-11-26 23:23:21 +0200281 results[sensor_name] = int(vals[pos])
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300282 elif dev_name == 'procs_blocked':
283 val = int(vals[1])
koder aka kdanilov70227062016-11-26 23:23:21 +0200284 results["cpu.procs_blocked"] = val
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300285 elif dev_name.startswith('cpu'):
286 core_count += 1
287
288 # procs in queue
289 TASKSPOS = 3
290 vals = open('/proc/loadavg').read().split()
291 ready_procs = vals[TASKSPOS].partition('/')[0]
koder aka kdanilov70227062016-11-26 23:23:21 +0200292
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300293 # dec on current proc
294 procs_queue = (float(ready_procs) - 1) / core_count
koder aka kdanilov70227062016-11-26 23:23:21 +0200295 results["cpu.procs_queue"] = procs_queue
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300296
297 return results
298
299
300# return this values or setted in allowed
301ram_fields = [
302 'MemTotal',
303 'MemFree',
304 'Buffers',
305 'Cached',
306 'SwapCached',
307 'Dirty',
308 'Writeback',
309 'SwapTotal',
310 'SwapFree'
311]
312
313
314@provides("system-ram")
315def sysram_stat(disallowed_prefixes=None, allowed_prefixes=None):
316 if allowed_prefixes is None:
317 allowed_prefixes = ram_fields
koder aka kdanilov70227062016-11-26 23:23:21 +0200318
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300319 results = {}
koder aka kdanilov70227062016-11-26 23:23:21 +0200320
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300321 for line in open('/proc/meminfo'):
322 vals = line.split()
323 dev_name = vals[0].rstrip(":")
324
325 dev_ok = is_dev_accepted(dev_name,
326 disallowed_prefixes,
327 allowed_prefixes)
328
329 title = "ram.{0}".format(dev_name)
330
331 if dev_ok:
koder aka kdanilov70227062016-11-26 23:23:21 +0200332 results[title] = int(vals[1])
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300333
334 if 'ram.MemFree' in results and 'ram.MemTotal' in results:
335 used = results['ram.MemTotal'].value - results['ram.MemFree'].value
koder aka kdanilov70227062016-11-26 23:23:21 +0200336 results["ram.usage_percent"] = int(float(used) / results['ram.MemTotal'].value)
337
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300338 return results
koder aka kdanilov70227062016-11-26 23:23:21 +0200339
340
341class SensorsData(object):
342 def __init__(self):
343 self.cond = threading.Condition()
344 self.collected_at = array.array("f")
345 self.stop = False
346 self.data = {} # map sensor_name to list of results
347 self.data_fd = None
348
349
350# TODO(koder): a lot code here can be optimized and cached, but nobody cares (c)
351def sensors_bg_thread(sensors_config, sdata):
352 next_collect_at = time.time()
353
354 while not sdata.stop:
355 dtime = next_collect_at - time.time()
356 if dtime > 0:
357 sdata.cond.wait(dtime)
358
359 if sdata.stop:
360 break
361
362 ctm = time.time()
363 curr = {}
364 for name, config in sensors_config.items():
365 params = {}
366
367 if "allow" in config:
368 params["allowed_prefixes"] = config["allow"]
369
370 if "disallow" in config:
371 params["disallowed_prefixes"] = config["disallow"]
372
373 curr[name] = SensorsMap[name](**params)
374
375 etm = time.time()
376
377 if etm - ctm > 0.1:
378 # TODO(koder): need to signal that something in not really ok with sensor collecting
379 pass
380
381 with sdata.cond:
382 sdata.collected_at.append(ctm)
383 for source_name, vals in curr.items():
384 for sensor_name, val in vals.items():
385 key = (source_name, sensor_name)
386 if key not in sdata.data:
387 sdata.data[key] = array.array("I", [val])
388 else:
389 sdata.data[key].append(val)
390
391
392sensors_thread = None
393sdata = None # type: SensorsData
394
395
396def rpc_start(sensors_config):
397 global sensors_thread
398 global sdata
399
400 if sensors_thread is not None:
401 raise ValueError("Thread already running")
402
403 sdata = SensorsData()
404 sensors_thread = threading.Thread(target=sensors_bg_thread, args=(sensors_config, sdata))
405 sensors_thread.daemon = True
406 sensors_thread.start()
407
408
409def rpc_get_updates():
410 if sdata is None:
411 raise ValueError("No sensor thread running")
412
413 with sdata.cond:
414 res = sdata.data
415 collected_at = sdata.collected_at
416 sdata.collected_at = array.array("f")
417 sdata.data = {name: array.array("I") for name in sdata.data}
418
419 return res, collected_at
420
421
422def rpc_stop():
423 global sensors_thread
424 global sdata
425
426 if sensors_thread is None:
427 raise ValueError("No sensor thread running")
428
429 sdata.stop = True
430 with sdata.cond:
431 sdata.cond.notify_all()
432
433 sensors_thread.join()
434 res = sdata.data
435 collected_at = sdata.collected_at
436
437 sensors_thread = None
438 sdata = None
439
440 return res, collected_at