mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 12:03:00 +00:00
@@ -12,6 +12,7 @@ import signal
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import statsd
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from distutils.version import LooseVersion as V
|
from distutils.version import LooseVersion as V
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
@@ -59,6 +60,10 @@ from .utils import (
|
|||||||
from .auth import Authenticator, PAMAuthenticator
|
from .auth import Authenticator, PAMAuthenticator
|
||||||
from .spawner import Spawner, LocalProcessSpawner
|
from .spawner import Spawner, LocalProcessSpawner
|
||||||
|
|
||||||
|
# For faking stats
|
||||||
|
from .emptyclass import EmptyClass
|
||||||
|
|
||||||
|
|
||||||
common_aliases = {
|
common_aliases = {
|
||||||
'log-level': 'Application.log_level',
|
'log-level': 'Application.log_level',
|
||||||
'f': 'JupyterHub.config_file',
|
'f': 'JupyterHub.config_file',
|
||||||
@@ -492,6 +497,22 @@ class JupyterHub(Application):
|
|||||||
help="Extra log handlers to set on JupyterHub logger",
|
help="Extra log handlers to set on JupyterHub logger",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statsd(self):
|
||||||
|
if hasattr(self, '_statsd'):
|
||||||
|
return self._statsd
|
||||||
|
if self.statsd_host:
|
||||||
|
self._statsd = statsd.StatsClient(
|
||||||
|
self.statsd_host,
|
||||||
|
self.statsd_port,
|
||||||
|
self.statsd_prefix
|
||||||
|
)
|
||||||
|
return self._statsd
|
||||||
|
else:
|
||||||
|
# return an empty mock object!
|
||||||
|
self._statsd = EmptyClass()
|
||||||
|
return self._statsd
|
||||||
|
|
||||||
def init_logging(self):
|
def init_logging(self):
|
||||||
# This prevents double log messages because tornado use a root logger that
|
# This prevents double log messages because tornado use a root logger that
|
||||||
# self.log is a child of. The logging module dipatches log messages to a log
|
# self.log is a child of. The logging module dipatches log messages to a log
|
||||||
@@ -991,6 +1012,7 @@ class JupyterHub(Application):
|
|||||||
version_hash=version_hash,
|
version_hash=version_hash,
|
||||||
subdomain_host=subdomain_host,
|
subdomain_host=subdomain_host,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
|
statsd=self.statsd,
|
||||||
)
|
)
|
||||||
# allow configured settings to have priority
|
# allow configured settings to have priority
|
||||||
settings.update(self.tornado_settings)
|
settings.update(self.tornado_settings)
|
||||||
@@ -1112,6 +1134,8 @@ class JupyterHub(Application):
|
|||||||
def update_last_activity(self):
|
def update_last_activity(self):
|
||||||
"""Update User.last_activity timestamps from the proxy"""
|
"""Update User.last_activity timestamps from the proxy"""
|
||||||
routes = yield self.proxy.get_routes()
|
routes = yield self.proxy.get_routes()
|
||||||
|
users_count = 0
|
||||||
|
active_users_count = 0
|
||||||
for prefix, route in routes.items():
|
for prefix, route in routes.items():
|
||||||
if 'user' not in route:
|
if 'user' not in route:
|
||||||
# not a user route, ignore it
|
# not a user route, ignore it
|
||||||
@@ -1125,6 +1149,12 @@ class JupyterHub(Application):
|
|||||||
except Exception:
|
except Exception:
|
||||||
dt = datetime.strptime(route['last_activity'], ISO8601_s)
|
dt = datetime.strptime(route['last_activity'], ISO8601_s)
|
||||||
user.last_activity = max(user.last_activity, dt)
|
user.last_activity = max(user.last_activity, dt)
|
||||||
|
# FIXME: Make this configurable duration. 30 minutes for now!
|
||||||
|
if (datetime.now() - user.last_activity).total_seconds() < 30 * 60:
|
||||||
|
active_users_count += 1
|
||||||
|
users_count += 1
|
||||||
|
self.statsd.gauge('users.running', users_count)
|
||||||
|
self.statsd.gauge('users.active', active_users_count)
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
yield self.proxy.check_routes(self.users, routes)
|
yield self.proxy.check_routes(self.users, routes)
|
||||||
|
13
jupyterhub/emptyclass.py
Normal file
13
jupyterhub/emptyclass.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
Simple empty class that returns itself for all functions called on it.
|
||||||
|
This allows us to call any method of any name on this, and it'll return another
|
||||||
|
instance of itself that'll allow any method to be called on it.
|
||||||
|
|
||||||
|
Primarily used to mock out the statsd client when statsd is not being used
|
||||||
|
"""
|
||||||
|
class EmptyClass:
|
||||||
|
def empty_function(self, *args, **kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return self.empty_function
|
@@ -75,6 +75,10 @@ class BaseHandler(RequestHandler):
|
|||||||
def proxy(self):
|
def proxy(self):
|
||||||
return self.settings['proxy']
|
return self.settings['proxy']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statsd(self):
|
||||||
|
return self.settings['statsd']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def authenticator(self):
|
def authenticator(self):
|
||||||
return self.settings.get('authenticator', None)
|
return self.settings.get('authenticator', None)
|
||||||
@@ -300,6 +304,7 @@ class BaseHandler(RequestHandler):
|
|||||||
return
|
return
|
||||||
toc = IOLoop.current().time()
|
toc = IOLoop.current().time()
|
||||||
self.log.info("User %s server took %.3f seconds to start", user.name, toc-tic)
|
self.log.info("User %s server took %.3f seconds to start", user.name, toc-tic)
|
||||||
|
self.statsd.timing('spawner.success', (toc - tic) * 1000)
|
||||||
yield self.proxy.add_user(user)
|
yield self.proxy.add_user(user)
|
||||||
user.spawner.add_poll_callback(self.user_stopped, user)
|
user.spawner.add_poll_callback(self.user_stopped, user)
|
||||||
|
|
||||||
@@ -323,6 +328,8 @@ class BaseHandler(RequestHandler):
|
|||||||
# schedule finish for when the user finishes spawning
|
# schedule finish for when the user finishes spawning
|
||||||
IOLoop.current().add_future(f, finish_user_spawn)
|
IOLoop.current().add_future(f, finish_user_spawn)
|
||||||
else:
|
else:
|
||||||
|
toc = IOLoop.current().time()
|
||||||
|
self.statsd.timing('spawner.failure', (toc - tic) * 1000)
|
||||||
raise web.HTTPError(500, "Spawner failed to start [status=%s]" % status)
|
raise web.HTTPError(500, "Spawner failed to start [status=%s]" % status)
|
||||||
else:
|
else:
|
||||||
yield finish_user_spawn()
|
yield finish_user_spawn()
|
||||||
@@ -471,6 +478,7 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
if current_user.spawner:
|
if current_user.spawner:
|
||||||
if current_user.spawn_pending:
|
if current_user.spawn_pending:
|
||||||
# spawn has started, but not finished
|
# spawn has started, but not finished
|
||||||
|
self.statsd.incr('redirects.user_spawn_pending', 1)
|
||||||
html = self.render_template("spawn_pending.html", user=current_user)
|
html = self.render_template("spawn_pending.html", user=current_user)
|
||||||
self.finish(html)
|
self.finish(html)
|
||||||
return
|
return
|
||||||
@@ -490,12 +498,15 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
if self.subdomain_host:
|
if self.subdomain_host:
|
||||||
target = current_user.host + target
|
target = current_user.host + target
|
||||||
self.redirect(target)
|
self.redirect(target)
|
||||||
|
self.statsd.incr('redirects.user_after_login')
|
||||||
elif current_user:
|
elif current_user:
|
||||||
# logged in as a different user, redirect
|
# logged in as a different user, redirect
|
||||||
|
self.statsd.incr('redirects.user_to_user', 1)
|
||||||
target = url_path_join(current_user.url, user_path or '')
|
target = url_path_join(current_user.url, user_path or '')
|
||||||
self.redirect(target)
|
self.redirect(target)
|
||||||
else:
|
else:
|
||||||
# not logged in, clear any cookies and reload
|
# not logged in, clear any cookies and reload
|
||||||
|
self.statsd.incr('redirects.user_to_login', 1)
|
||||||
self.clear_login_cookie()
|
self.clear_login_cookie()
|
||||||
self.redirect(url_concat(
|
self.redirect(url_concat(
|
||||||
self.settings['login_url'],
|
self.settings['login_url'],
|
||||||
@@ -508,8 +519,12 @@ class CSPReportHandler(BaseHandler):
|
|||||||
@web.authenticated
|
@web.authenticated
|
||||||
def post(self):
|
def post(self):
|
||||||
'''Log a content security policy violation report'''
|
'''Log a content security policy violation report'''
|
||||||
self.log.warning("Content security violation: %s",
|
self.log.warning(
|
||||||
self.request.body.decode('utf8', 'replace'))
|
"Content security violation: %s",
|
||||||
|
self.request.body.decode('utf8', 'replace')
|
||||||
|
)
|
||||||
|
# Report it to statsd as well
|
||||||
|
self.statsd.incr('csp_report')
|
||||||
|
|
||||||
default_handlers = [
|
default_handlers = [
|
||||||
(r'/user/([^/]+)(/.*)?', UserSpawnHandler),
|
(r'/user/([^/]+)(/.*)?', UserSpawnHandler),
|
||||||
|
@@ -20,6 +20,7 @@ class LogoutHandler(BaseHandler):
|
|||||||
self.clear_login_cookie(name)
|
self.clear_login_cookie(name)
|
||||||
user.other_user_cookies = set([])
|
user.other_user_cookies = set([])
|
||||||
self.redirect(self.hub.server.base_url, permanent=False)
|
self.redirect(self.hub.server.base_url, permanent=False)
|
||||||
|
self.statsd.incr('logout')
|
||||||
|
|
||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
class LoginHandler(BaseHandler):
|
||||||
@@ -35,6 +36,7 @@ class LoginHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
|
self.statsd.incr('login.request')
|
||||||
next_url = self.get_argument('next', '')
|
next_url = self.get_argument('next', '')
|
||||||
if not next_url.startswith('/'):
|
if not next_url.startswith('/'):
|
||||||
# disallow non-absolute next URLs (e.g. full URLs)
|
# disallow non-absolute next URLs (e.g. full URLs)
|
||||||
@@ -61,8 +63,13 @@ class LoginHandler(BaseHandler):
|
|||||||
for arg in self.request.arguments:
|
for arg in self.request.arguments:
|
||||||
data[arg] = self.get_argument(arg)
|
data[arg] = self.get_argument(arg)
|
||||||
|
|
||||||
|
auth_timer = self.statsd.timer('login.authenticate').start()
|
||||||
username = yield self.authenticate(data)
|
username = yield self.authenticate(data)
|
||||||
|
auth_timer.stop(send=False)
|
||||||
|
|
||||||
if username:
|
if username:
|
||||||
|
self.statsd.incr('login.success')
|
||||||
|
self.statsd.timing('login.authenticate.success', auth_timer.ms)
|
||||||
user = self.user_from_username(username)
|
user = self.user_from_username(username)
|
||||||
already_running = False
|
already_running = False
|
||||||
if user.spawner:
|
if user.spawner:
|
||||||
@@ -78,6 +85,8 @@ class LoginHandler(BaseHandler):
|
|||||||
self.redirect(next_url)
|
self.redirect(next_url)
|
||||||
self.log.info("User logged in: %s", username)
|
self.log.info("User logged in: %s", username)
|
||||||
else:
|
else:
|
||||||
|
self.statsd.incr('login.failure')
|
||||||
|
self.statsd.timing('login.authenticate.failure', auth_timer.ms)
|
||||||
self.log.debug("Failed login for %s", data.get('username', 'unknown user'))
|
self.log.debug("Failed login for %s", data.get('username', 'unknown user'))
|
||||||
html = self._render(
|
html = self._render(
|
||||||
login_error='Invalid username or password',
|
login_error='Invalid username or password',
|
||||||
|
@@ -2,5 +2,6 @@ traitlets>=4.1
|
|||||||
tornado>=4.1
|
tornado>=4.1
|
||||||
jinja2
|
jinja2
|
||||||
pamela
|
pamela
|
||||||
|
statsd
|
||||||
sqlalchemy>=1.0
|
sqlalchemy>=1.0
|
||||||
requests
|
requests
|
||||||
|
Reference in New Issue
Block a user