Skip to content

Commit edafd63

Browse files
committed
Right now all of the configuration options remain backwards-compatible with previous versions. However this seems to now be sufficiently abstract as to be at the cusp of rapid 'expandability' in terms of its features. The next step will be to automatically make as many prometheus Gauge's as needed to measure every specificied metric, i.e. without defining them all manually. setattr will be useful for this I believe.
1 parent caacae0 commit edafd63

File tree

4 files changed

+129
-98
lines changed

4 files changed

+129
-98
lines changed

nbresuse/__init__.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
from tornado import ioloop
44
from traitlets import Bool
55
from traitlets import default
6+
from traitlets import Dict
67
from traitlets import Float
78
from traitlets import Int
9+
from traitlets import List
10+
from traitlets import TraitType
811
from traitlets import Union
912
from traitlets.config import Configurable
1013

14+
from nbresuse.metrics import PSUtilMetricsLoader
1115
from nbresuse.prometheus import PrometheusHandler
1216

1317
try:
@@ -17,6 +21,21 @@
1721
from .utils import Callable
1822

1923

24+
class PSUtilMetric(TraitType):
25+
"""A trait describing the format to specify a metric from the psutil package"""
26+
27+
info_text = "A dictionary specifying the function/method name, any keyword arguments, and if a named tuple is returned, which attribute of the named tuple to select"
28+
29+
def validate(self, obj, value):
30+
if isinstance(value, dict):
31+
keys = list(value.keys())
32+
if "name" in keys:
33+
keys.remove("name")
34+
if all(key in ["kwargs", "attribute"] for key in keys):
35+
return value
36+
self.error(obj, value)
37+
38+
2039
def _jupyter_server_extension_paths():
2140
"""
2241
Set up the server extension for collecting metrics
@@ -43,6 +62,25 @@ class ResourceUseDisplay(Configurable):
4362
Holds server-side configuration for nbresuse
4463
"""
4564

65+
process_memory_metrics = List(
66+
trait=PSUtilMetric(),
67+
default_value=[{"name": "memory_info", "attribute": "rss"}],
68+
)
69+
70+
system_memory_metrics = List(
71+
trait=PSUtilMetric(),
72+
default_value=[{"name": "virtual_memory", "attribute": "total"}],
73+
)
74+
75+
process_cpu_metrics = List(
76+
trait=PSUtilMetric(),
77+
default_value=[{"name": "cpu_percent", "kwargs": {"interval": 0.05}}],
78+
)
79+
80+
system_cpu_metrics = List(
81+
trait=PSUtilMetric(), default_value=[{"name": "cpu_count"}]
82+
)
83+
4684
mem_warning_threshold = Float(
4785
default_value=0.1,
4886
help="""
@@ -117,5 +155,7 @@ def load_jupyter_server_extension(nbapp):
117155
"""
118156
resuseconfig = ResourceUseDisplay(parent=nbapp)
119157
nbapp.web_app.settings["nbresuse_display_config"] = resuseconfig
120-
callback = ioloop.PeriodicCallback(PrometheusHandler(nbapp), 1000)
158+
callback = ioloop.PeriodicCallback(
159+
PrometheusHandler(PSUtilMetricsLoader(nbapp)), 1000
160+
)
121161
callback.start()

nbresuse/metrics.py

Lines changed: 67 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,81 @@
1-
from typing import NamedTuple
2-
from typing import Optional
3-
41
try:
52
import psutil
63
except ImportError:
74
psutil = None
85

9-
10-
class MemoryMetrics(NamedTuple):
11-
rss: int
12-
virtual_memory: int
13-
14-
15-
class CPUMetrics(NamedTuple):
16-
cpu_percent: float
17-
cpu_count: int
18-
19-
20-
def per_process_metric(metric_name, metric_kwargs={}, metric_attribute=None):
21-
if psutil is None:
22-
return None
23-
else:
24-
current_process = psutil.Process()
25-
all_processes = [current_process] + current_process.children(recursive=True)
26-
27-
def get_per_process_metric(
28-
process, metric_name, metric_kwargs, metric_attribute=None
29-
):
30-
try:
31-
metric_value = getattr(process, metric_name)(**metric_kwargs)
32-
if metric_attribute is not None:
33-
return getattr(metric_value, metric_attribute)
6+
from notebook.notebookapp import NotebookApp
7+
8+
9+
class PSUtilMetricsLoader:
10+
def __init__(self, nbapp: NotebookApp):
11+
self.config = nbapp.web_app.settings["nbresuse_display_config"]
12+
self.nbapp = nbapp
13+
14+
def process_metric(self, name, kwargs={}, attribute=None):
15+
if psutil is None:
16+
return None
17+
else:
18+
current_process = psutil.Process()
19+
all_processes = [current_process] + current_process.children(recursive=True)
20+
21+
def get_process_metric(process, name, kwargs, attribute=None):
22+
try:
23+
# psutil.Process methods will either return...
24+
metric_value = getattr(process, name)(**kwargs)
25+
if attribute is not None: # ... a named tuple
26+
return getattr(metric_value, attribute)
27+
else: # ... or a number
28+
return metric_value
29+
# Avoid littering logs with stack traces
30+
# complaining about dead processes
31+
except BaseException:
32+
return 0
33+
34+
process_metric_value = lambda process: get_process_metric(
35+
process, name, kwargs, attribute
36+
)
37+
38+
return sum([process_metric_value(process) for process in all_processes])
39+
40+
def system_metric(self, name, kwargs={}, attribute=None):
41+
if psutil is None:
42+
return None
43+
else:
44+
# psutil functions will either return...
45+
metric_value = getattr(psutil, name)(**kwargs)
46+
if attribute is not None: # ... a named tuple
47+
return getattr(metric_value, attribute)
48+
else: # ... or a number
3449
return metric_value
35-
# Avoid littering logs with stack traces
36-
# complaining about dead processes
37-
except BaseException:
38-
return 0
39-
40-
per_process_metric_value = lambda process: get_per_process_metric(
41-
process, metric_name, metric_kwargs, metric_attribute
42-
)
43-
44-
return sum([per_process_metric_value(process) for process in all_processes])
4550

51+
def get_metric_values(self, metrics, metric_type):
52+
metric_types = {"process": self.process_metric, "system": self.system_metric}
53+
metric_value = metric_types[metric_type] # Switch statement
4654

47-
def system_metric(metric_name, metric_kwargs={}, metric_attribute=None):
48-
if psutil is None:
49-
return None
50-
else:
51-
metric_value = getattr(psutil, metric_name)(**metric_kwargs)
52-
if metric_attribute is not None:
53-
return getattr(metric_value, metric_attribute)
54-
return metric_attribute
55+
metric_values = {}
56+
for metric in metrics:
57+
name = metric["name"]
58+
if metric.get("attribute", False):
59+
name += "_" + metric.get("attribute")
60+
metric_values.update({name: metric_value(**metric)})
61+
return metric_values
5562

63+
def metrics(self, process_metrics, system_metrics):
5664

57-
def memory_metrics() -> Optional[MemoryMetrics]:
65+
metric_values = self.get_metric_values(process_metrics, "process")
66+
metric_values.update(self.get_metric_values(system_metrics, "system"))
5867

59-
rss = {"metric_name": "memory_info", "metric_attribute": "rss"}
60-
rss_value = per_process_metric(**rss)
68+
if any(value is None for value in metric_values.values()):
69+
return None
6170

62-
virtual_memory = {"metric_name": "virtual_memory", "metric_attribute": "total"}
63-
virtual_memory_value = system_metric(**virtual_memory)
71+
return metric_values
6472

65-
memory_metric_values = {"rss": rss_value, "virtual_memory": virtual_memory_value}
66-
67-
if any(value is None for value in memory_metric_values.values()):
68-
return None
69-
70-
return MemoryMetrics(**memory_metric_values)
71-
72-
73-
def cpu_metrics() -> Optional[CPUMetrics]:
74-
75-
cpu_percent = {"metric_name": "cpu_percent", "metric_kwargs": {"interval": 0.05}}
76-
cpu_percent_value = per_process_metric(**cpu_percent)
77-
78-
cpu_count = {"metric_name": "cpu_count"}
79-
cpu_count_value = system_metric(**cpu_count)
80-
81-
cpu_metric_values = {"cpu_percent": cpu_percent_value, "cpu_count": cpu_count_value}
82-
83-
if any(value is None for value in cpu_metric_values.values()):
84-
return None
73+
def memory_metrics(self):
74+
return self.metrics(
75+
self.config.process_memory_metrics, self.config.system_memory_metrics
76+
)
8577

86-
return CPUMetrics(**cpu_metric_values)
78+
def cpu_metrics(self):
79+
return self.metrics(
80+
self.config.process_cpu_metrics, self.config.system_cpu_metrics
81+
)

nbresuse/prometheus.py

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@
22

33
from notebook.notebookapp import NotebookApp
44
from prometheus_client import Gauge
5-
from tornado import gen
65

7-
from nbresuse.metrics import cpu_metrics
8-
from nbresuse.metrics import CPUMetrics
9-
from nbresuse.metrics import memory_metrics
10-
from nbresuse.metrics import MemoryMetrics
6+
from nbresuse.metrics import PSUtilMetricsLoader
117

128
try:
139
# Traitlets >= 4.3.3
@@ -17,10 +13,11 @@
1713

1814

1915
class PrometheusHandler(Callable):
20-
def __init__(self, nbapp: NotebookApp):
16+
def __init__(self, metricsloader: PSUtilMetricsLoader):
2117
super().__init__()
22-
self.config = nbapp.web_app.settings["nbresuse_display_config"]
23-
self.session_manager = nbapp.session_manager
18+
self.metricsloader = metricsloader
19+
self.config = metricsloader.config
20+
self.session_manager = metricsloader.nbapp.session_manager
2421

2522
self.TOTAL_MEMORY_USAGE = Gauge(
2623
"total_memory_usage", "counter for total memory usage", []
@@ -34,40 +31,39 @@ def __init__(self, nbapp: NotebookApp):
3431
)
3532
self.MAX_CPU_USAGE = Gauge("max_cpu_usage", "counter for max cpu usage", [])
3633

37-
@gen.coroutine
38-
def __call__(self, *args, **kwargs):
39-
memory_metric_values = memory_metrics()
34+
async def __call__(self, *args, **kwargs):
35+
memory_metric_values = self.metricsloader.memory_metrics()
4036
if memory_metric_values is not None:
41-
self.TOTAL_MEMORY_USAGE.set(memory_metric_values.rss)
37+
self.TOTAL_MEMORY_USAGE.set(memory_metric_values["memory_info_rss"])
4238
self.MAX_MEMORY_USAGE.set(self.apply_memory_limit(memory_metric_values))
4339
if self.config.track_cpu_percent:
44-
cpu_metric_values = cpu_metrics()
40+
cpu_metric_values = self.metricsloader.cpu_metrics()
4541
if cpu_metric_values is not None:
46-
self.TOTAL_CPU_USAGE.set(cpu_metric_values.cpu_percent)
42+
self.TOTAL_CPU_USAGE.set(cpu_metric_values["cpu_percent"])
4743
self.MAX_CPU_USAGE.set(self.apply_cpu_limit(cpu_metric_values))
4844

49-
def apply_memory_limit(
50-
self, memory_metric_values: Optional[MemoryMetrics]
51-
) -> Optional[int]:
45+
def apply_memory_limit(self, memory_metric_values) -> Optional[int]:
5246
if memory_metric_values is None:
5347
return None
5448
else:
5549
if callable(self.config.mem_limit):
56-
return self.config.mem_limit(rss=memory_metric_values.rss)
50+
return self.config.mem_limit(
51+
rss=memory_metric_values["memory_info_rss"]
52+
)
5753
elif self.config.mem_limit > 0: # mem_limit is an Int
5854
return self.config.mem_limit
5955
else:
60-
return memory_metric_values.virtual_memory
56+
return memory_metric_values["virtual_memory_total"]
6157

62-
def apply_cpu_limit(
63-
self, cpu_metric_values: Optional[CPUMetrics]
64-
) -> Optional[float]:
58+
def apply_cpu_limit(self, cpu_metric_values) -> Optional[float]:
6559
if cpu_metric_values is None:
6660
return None
6761
else:
6862
if callable(self.config.cpu_limit):
69-
return self.config.cpu_limit(cpu_percent=cpu_metric_values.cpu_percent)
63+
return self.config.cpu_limit(
64+
cpu_percent=cpu_metric_values["cpu_percent"]
65+
)
7066
elif self.config.cpu_limit > 0.0: # cpu_limit is a Float
7167
return self.config.cpu_limit
7268
else:
73-
return 100.0 * cpu_metric_values.cpu_count
69+
return 100.0 * cpu_metric_values["cpu_count"]

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setuptools.setup(
1313
name="nbresuse",
14-
version="0.3.4",
14+
version="0.4.0",
1515
url="https://github.com/yuvipanda/nbresuse",
1616
author="Yuvi Panda",
1717
description="Simple Jupyter extension to show how much resources (RAM) your notebook is using",

0 commit comments

Comments
 (0)