Skip to content

Commit 80c31ea

Browse files
committed
Merge pull request prometheus#19 from brian-brazil/standard-exports
Standard exports
2 parents 13739f1 + efbabea commit 80c31ea

File tree

13 files changed

+183
-3
lines changed

13 files changed

+183
-3
lines changed

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Maintainers of this repository:
55
The following individuals have contributed code to this repository
66
(listed in alphabetical order):
77

8+
* Andrea Fagan <andreafagan28@gmail.com>
89
* Brian Brazil <brian.brazil@gmail.com>

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ c.labels('get', '/').inc()
133133
c.labels('post', '/submit').inc()
134134
```
135135

136+
### Process Collector
137+
138+
The Python Client automatically exports metrics about process CPU usage, RAM,
139+
file descriptors and start time. These all have the prefix `process\_`, and
140+
are only currently available on Linux.
141+
136142
## Exporting
137143

138144
There are several options for exporting metrics.
@@ -180,4 +186,5 @@ g.set(1)
180186
write_to_textfile('/configured/textfile/path/raid.prom', registry)
181187
```
182188

183-
A separate registry is used, as the default registry may contain other metrics.
189+
A separate registry is used, as the default registry may contain other metrics
190+
such as those from the Process Collector.

prometheus_client/__init__.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import copy
66
import re
7+
import resource
78
import os
89
import time
910
import threading
@@ -419,7 +420,7 @@ def generate_latest(registry=REGISTRY):
419420
labelstr = '{{{0}}}'.format(','.join(
420421
['{0}="{1}"'.format(
421422
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
422-
for k, v in labels.items()]))
423+
for k, v in sorted(labels.items())]))
423424
else:
424425
labelstr = ''
425426
output.append('{0}{1} {2}\n'.format(name, labelstr, _floatToGoString(value)))
@@ -446,6 +447,83 @@ def write_to_textfile(path, registry):
446447
os.rename(tmppath, path)
447448

448449

450+
class ProcessCollector(object):
451+
"""Collector for Standard Exports such as cpu and memory."""
452+
def __init__(self, namespace='', pid='self', proc='/proc', registry=REGISTRY):
453+
self._namespace = namespace
454+
self._pid = os.path.join(proc, str(pid))
455+
self._proc = proc
456+
self._pagesize = resource.getpagesize()
457+
if namespace:
458+
self._prefix = namespace + '_process_'
459+
else:
460+
self._prefix = 'process_'
461+
self._ticks = 100.0
462+
try:
463+
self._ticks = os.sysconf('SC_CLK_TCK')
464+
except (ValueError, TypeError):
465+
pass
466+
467+
# This is used to test if we can access /proc.
468+
self._btime = 0
469+
try:
470+
self._btime = self._boot_time()
471+
except IOError:
472+
pass
473+
if registry:
474+
registry.register(self)
475+
476+
def _boot_time(self):
477+
with open(os.path.join(self._proc, 'stat')) as stat:
478+
for line in stat:
479+
if line.startswith('btime '):
480+
return float(line.split()[1])
481+
482+
def collect(self):
483+
if not self._btime:
484+
return []
485+
486+
result = []
487+
try:
488+
with open(os.path.join(self._pid, 'stat')) as stat:
489+
parts = (stat.read().split(')')[-1].split())
490+
vmem = Metric(self._prefix + 'virtual_memory_bytes', 'Virtual memory size in bytes', 'gauge')
491+
vmem.add_sample(self._prefix + 'virtual_memory_bytes', {}, float(parts[20]))
492+
rss = Metric(self._prefix + 'resident_memory_bytes', 'Resident memory size in bytes', 'gauge')
493+
rss.add_sample(self._prefix + 'resident_memory_bytes', {}, float(parts[21]) * self._pagesize)
494+
start_time = Metric(self._prefix + 'start_time_seconds',
495+
'Start time of the process since unix epoch in seconds.', 'gauge')
496+
start_time_secs = float(parts[19]) / self._ticks
497+
start_time.add_sample(self._prefix + 'start_time_seconds',{} , start_time_secs + self._btime)
498+
utime = float(parts[11]) / self._ticks
499+
stime = float(parts[12]) / self._ticks
500+
cpu = Metric(self._prefix + 'cpu_seconds_total',
501+
'Total user and system CPU time spent in seconds.', 'counter')
502+
cpu.add_sample(self._prefix + 'cpu_seconds_total', {}, utime + stime)
503+
result.extend([vmem, rss, start_time, cpu])
504+
except IOError:
505+
pass
506+
507+
try:
508+
max_fds = Metric(self._prefix + 'max_fds', 'Maximum number of open file descriptors.', 'gauge')
509+
with open(os.path.join(self._pid, 'limits')) as limits:
510+
for line in limits:
511+
if line.startswith('Max open file'):
512+
max_fds.add_sample(self._prefix + 'max_fds', {}, float(line.split()[3]))
513+
break
514+
open_fds = Metric(self._prefix + 'open_fds', 'Number of open file descriptors.', 'gauge')
515+
open_fds.add_sample(self._prefix + 'open_fds', {}, len(os.listdir(os.path.join(self._pid, 'fd'))))
516+
result.extend([open_fds, max_fds])
517+
except IOError:
518+
pass
519+
520+
return result
521+
522+
523+
PROCESS_COLLECTOR = ProcessCollector()
524+
"""Default ProcessCollector in default Registry REGISTRY."""
525+
526+
449527
if __name__ == '__main__':
450528
c = Counter('cc', 'A counter')
451529
c.inc()

tests/proc/26231/fd/0

Whitespace-only changes.

tests/proc/26231/fd/1

Whitespace-only changes.

tests/proc/26231/fd/2

Whitespace-only changes.

tests/proc/26231/fd/3

Whitespace-only changes.

tests/proc/26231/fd/4

Whitespace-only changes.

tests/proc/26231/limits

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Limit Soft Limit Hard Limit Units
2+
Max cpu time unlimited unlimited seconds
3+
Max file size unlimited unlimited bytes
4+
Max data size unlimited unlimited bytes
5+
Max stack size 8388608 unlimited bytes
6+
Max core file size 0 unlimited bytes
7+
Max resident set unlimited unlimited bytes
8+
Max processes 62898 62898 processes
9+
Max open files 2048 4096 files
10+
Max locked memory 65536 65536 bytes
11+
Max address space unlimited unlimited bytes
12+
Max file locks unlimited unlimited locks
13+
Max pending signals 62898 62898 signals
14+
Max msgqueue size 819200 819200 bytes
15+
Max nice priority 0 0

tests/proc/26231/stat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
26231 (vim) R 5392 7446 5392 34835 7446 4218880 32533 309516 26 82 1677 44 158 99 20 0 1 0 82375 56274944 1981 18446744073709551615 4194304 6294284 140736914091744 140736914087944 139965136429984 0 0 12288 1870679807 0 0 0 17 0 0 0 31 0 0 8391624 8481048 16420864 140736914093252 140736914093279 140736914093279 140736914096107 0

tests/proc/584/stat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1020 ((a b ) ( c d) ) R 28378 1020 28378 34842 1020 4218880 286 0 0 0 0 0 0 0 20 0 1 0 10839175 10395648 155 18446744073709551615 4194304 4238788 140736466511168 140736466511168 140609271124624 0 0 0 0 0 0 0 17 5 0 0 0 0 0 6336016 6337300 25579520 140736466515030 140736466515061 140736466515061 140736466518002 0

tests/proc/stat

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
cpu 301854 612 111922 8979004 3552 2 3944 0 0 0
2+
cpu0 44490 19 21045 1087069 220 1 3410 0 0 0
3+
cpu1 47869 23 16474 1110787 591 0 46 0 0 0
4+
cpu2 46504 36 15916 1112321 441 0 326 0 0 0
5+
cpu3 47054 102 15683 1113230 533 0 60 0 0 0
6+
cpu4 28413 25 10776 1140321 217 0 8 0 0 0
7+
cpu5 29271 101 11586 1136270 672 0 30 0 0 0
8+
cpu6 29152 36 10276 1139721 319 0 29 0 0 0
9+
cpu7 29098 268 10164 1139282 555 0 31 0 0 0
10+
intr 8885917 17 0 0 0 0 0 0 0 1 79281 0 0 0 0 0 0 0 231237 0 0 0 0 250586 103 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 223424 190745 13 906 1283803 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0intr 8885917 17 0 0 0 0 0 0 0 1 79281 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
11+
ctxt 38014093
12+
btime 1418183276
13+
processes 26442
14+
procs_running 2
15+
procs_blocked 0
16+
softirq 5057579 250191 1481983 1647 211099 186066 0 1783454 622196 12499 508444

tests/test_client.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import unicode_literals
2+
import os
23
import unittest
34

5+
46
from prometheus_client import Gauge, Counter, Summary, Histogram
5-
from prometheus_client import CollectorRegistry, generate_latest
7+
from prometheus_client import CollectorRegistry, generate_latest, ProcessCollector
68

79

810
class TestCounter(unittest.TestCase):
@@ -266,5 +268,64 @@ def test_escaping(self):
266268
self.assertEqual(b'# HELP cc A\\ncount\\\\er\n# TYPE cc counter\ncc{a="\\\\x\\n\\""} 1.0\n', generate_latest(self.registry))
267269

268270

271+
class TestProcessCollector(unittest.TestCase):
272+
def setUp(self):
273+
self.registry = CollectorRegistry()
274+
self.test_proc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'proc')
275+
276+
def test_working(self):
277+
collector = ProcessCollector(proc=self.test_proc, pid=26231, registry=self.registry)
278+
collector._pagesize = 4096
279+
collector._ticks = 100
280+
281+
self.assertEqual(17.21, self.registry.get_sample_value('process_cpu_seconds_total'))
282+
self.assertEqual(56274944.0, self.registry.get_sample_value('process_virtual_memory_bytes'))
283+
self.assertEqual(8114176, self.registry.get_sample_value('process_resident_memory_bytes'))
284+
self.assertEqual(1418184099.75, self.registry.get_sample_value('process_start_time_seconds'))
285+
self.assertEqual(2048.0, self.registry.get_sample_value('process_max_fds'))
286+
self.assertEqual(5.0, self.registry.get_sample_value('process_open_fds'))
287+
self.assertEqual(None, self.registry.get_sample_value('process_fake_namespace'))
288+
289+
def test_namespace(self):
290+
collector = ProcessCollector(proc=self.test_proc, pid=26231, registry=self.registry, namespace='n')
291+
collector._pagesize = 4096
292+
collector._ticks = 100
293+
294+
self.assertEqual(17.21, self.registry.get_sample_value('n_process_cpu_seconds_total'))
295+
self.assertEqual(56274944.0, self.registry.get_sample_value('n_process_virtual_memory_bytes'))
296+
self.assertEqual(8114176, self.registry.get_sample_value('n_process_resident_memory_bytes'))
297+
self.assertEqual(1418184099.75, self.registry.get_sample_value('n_process_start_time_seconds'))
298+
self.assertEqual(2048.0, self.registry.get_sample_value('n_process_max_fds'))
299+
self.assertEqual(5.0, self.registry.get_sample_value('n_process_open_fds'))
300+
self.assertEqual(None, self.registry.get_sample_value('process_cpu_seconds_total'))
301+
302+
def test_working_584(self):
303+
collector = ProcessCollector(proc=self.test_proc, pid=584, registry=self.registry)
304+
collector._pagesize = 4096
305+
collector._ticks = 100
306+
307+
self.assertEqual(0.0, self.registry.get_sample_value('process_cpu_seconds_total'))
308+
self.assertEqual(10395648.0, self.registry.get_sample_value('process_virtual_memory_bytes'))
309+
self.assertEqual(634880, self.registry.get_sample_value('process_resident_memory_bytes'))
310+
self.assertEqual(1418291667.75, self.registry.get_sample_value('process_start_time_seconds'))
311+
self.assertEqual(None, self.registry.get_sample_value('process_max_fds'))
312+
self.assertEqual(None, self.registry.get_sample_value('process_open_fds'))
313+
314+
def test_working_fake_pid(self):
315+
collector = ProcessCollector(proc=self.test_proc, pid=123, registry=self.registry)
316+
collector._pagesize = 4096
317+
collector._ticks = 100
318+
319+
self.assertEqual(None, self.registry.get_sample_value('process_cpu_seconds_total'))
320+
self.assertEqual(None, self.registry.get_sample_value('process_virtual_memory_bytes'))
321+
self.assertEqual(None, self.registry.get_sample_value('process_resident_memory_bytes'))
322+
self.assertEqual(None, self.registry.get_sample_value('process_start_time_seconds'))
323+
self.assertEqual(None, self.registry.get_sample_value('process_max_fds'))
324+
self.assertEqual(None, self.registry.get_sample_value('process_open_fds'))
325+
self.assertEqual(None, self.registry.get_sample_value('process_fake_namespace'))
326+
327+
328+
329+
269330
if __name__ == '__main__':
270331
unittest.main()

0 commit comments

Comments
 (0)