11import os
2- import json
3- import psutil
2+
3+ from tornado import ioloop
44from traitlets import Bool , Float , Int , Union , default
55from traitlets .config import Configurable
6- from notebook .utils import url_path_join
7- from notebook .base .handlers import IPythonHandler
8- from tornado import web
6+
7+ from nbresuse .prometheus import PrometheusHandler
98
109try :
1110 # Traitlets >= 4.3.3
1211 from traitlets import Callable
1312except ImportError :
1413 from .utils import Callable
1514
16- from concurrent .futures import ThreadPoolExecutor
17- from tornado .concurrent import run_on_executor
18-
19- class MetricsHandler (IPythonHandler ):
20- def initialize (self ):
21- super ().initialize ()
22- self .cpu_percent = 0
23-
24- # https://www.tornadoweb.org/en/stable/concurrent.html#tornado.concurrent.run_on_executor
25- self .executor = ThreadPoolExecutor (max_workers = 10 )
26-
27- self .cpu_count = psutil .cpu_count ()
28-
29- @run_on_executor
30- def update_cpu_percent (self , all_processes ):
31-
32- def get_cpu_percent (p ):
33- try :
34- return p .cpu_percent (interval = 0.05 )
35- # Avoid littering logs with stack traces complaining
36- # about dead processes having no CPU usage
37- except :
38- return 0
39-
40- return sum ([get_cpu_percent (p ) for p in all_processes ])
41-
42- @web .authenticated
43- async def get (self ):
44- """
45- Calculate and return current resource usage metrics
46- """
47- config = self .settings ['nbresuse_display_config' ]
48- cur_process = psutil .Process ()
49- all_processes = [cur_process ] + cur_process .children (recursive = True )
50- limits = {}
51-
52- # Get memory information
53- rss = sum ([p .memory_info ().rss for p in all_processes ])
54-
55- if callable (config .mem_limit ):
56- mem_limit = config .mem_limit (rss = rss )
57- else : # mem_limit is an Int
58- mem_limit = config .mem_limit
59-
60- # A better approach would use cpu_affinity to account for the
61- # fact that the number of logical CPUs in the system is not
62- # necessarily the same as the number of CPUs the process
63- # can actually use. But cpu_affinity isn't available for OS X.
64- cpu_count = psutil .cpu_count ()
65-
66- if config .track_cpu_percent :
67- self .cpu_percent = await self .update_cpu_percent (all_processes )
68-
69- if config .mem_limit != 0 :
70- limits ['memory' ] = {
71- 'rss' : mem_limit
72- }
73- if config .mem_warning_threshold != 0 :
74- limits ['memory' ]['warn' ] = (mem_limit - rss ) < (mem_limit * config .mem_warning_threshold )
75-
76- # Optionally get CPU information
77- if config .track_cpu_percent :
78- self .cpu_percent = await self .update_cpu_percent (all_processes )
79-
80- if config .cpu_limit != 0 :
81- limits ['cpu' ] = {
82- 'cpu' : config .cpu_limit
83- }
84- if config .cpu_warning_threshold != 0 :
85- limits ['cpu' ]['warn' ] = (config .cpu_limit - self .cpu_percent ) < (config .cpu_limit * config .cpu_warning_threshold )
86-
87- metrics = {
88- 'rss' : rss ,
89- 'limits' : limits ,
90- }
91- if config .track_cpu_percent :
92- metrics .update (cpu_percent = self .cpu_percent ,
93- cpu_count = self .cpu_count )
94-
95- self .log .debug ("NBResuse metrics: %s" , metrics )
96- self .write (json .dumps (metrics ))
97-
9815
9916def _jupyter_server_extension_paths ():
10017 """
@@ -104,6 +21,7 @@ def _jupyter_server_extension_paths():
10421 'module' : 'nbresuse' ,
10522 }]
10623
24+
10725def _jupyter_nbextension_paths ():
10826 """
10927 Set up the notebook extension for displaying metrics
@@ -115,6 +33,7 @@ def _jupyter_nbextension_paths():
11533 "require" : "nbresuse/main"
11634 }]
11735
36+
11837class ResourceUseDisplay (Configurable ):
11938 """
12039 Holds server-side configuration for nbresuse
@@ -142,7 +61,7 @@ class ResourceUseDisplay(Configurable):
14261 Note that this does not actually limit the user's memory usage!
14362
14463 Defaults to reading from the `MEM_LIMIT` environment variable. If
145- set to 0, no memory limit is displayed.
64+ set to 0, the max memory available is displayed.
14665 """
14766 ).tag (config = True )
14867
@@ -151,7 +70,7 @@ def _mem_limit_default(self):
15170 return int (os .environ .get ('MEM_LIMIT' , 0 ))
15271
15372 track_cpu_percent = Bool (
154- default_value = False ,
73+ default_value = True ,
15574 help = """
15675 Set to True in order to enable reporting of CPU usage statistics.
15776 """
@@ -178,19 +97,20 @@ def _mem_limit_default(self):
17897 Note that this does not actually limit the user's CPU usage!
17998
18099 Defaults to reading from the `CPU_LIMIT` environment variable. If
181- set to 0, no CPU usage limit is displayed.
100+ set to 0, the total CPU count available is displayed.
182101 """
183102 ).tag (config = True )
184103
185104 @default ('cpu_limit' )
186105 def _cpu_limit_default (self ):
187106 return float (os .environ .get ('CPU_LIMIT' , 0 ))
188107
108+
189109def load_jupyter_server_extension (nbapp ):
190110 """
191111 Called during notebook start
192112 """
193113 resuseconfig = ResourceUseDisplay (parent = nbapp )
194114 nbapp .web_app .settings ['nbresuse_display_config' ] = resuseconfig
195- route_pattern = url_path_join ( nbapp . web_app . settings [ 'base_url' ], '/metrics' )
196- nbapp . web_app . add_handlers ( '.*' , [( route_pattern , MetricsHandler )] )
115+ callback = ioloop . PeriodicCallback ( PrometheusHandler ( nbapp ), 1000 )
116+ callback . start ( )
0 commit comments