Skip to content

Commit fe7eee0

Browse files
committed
Unify all logging to a central log which is an attribute of the NBViewer class. Update format of logs to resemble that of other Jupyter applications, and to be more informative. Patch some GitHub bugs.
1 parent 03238da commit fe7eee0

File tree

12 files changed

+176
-117
lines changed

12 files changed

+176
-117
lines changed

nbviewer/app.py

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
1919

20-
from tornado import web, httpserver, ioloop, log
20+
from tornado import web, httpserver, ioloop
21+
from tornado.log import access_log, app_log, LogFormatter
22+
from tornado.curl_httpclient import curl_log
2123

2224
import tornado.options
2325
from tornado.options import define, options
@@ -50,8 +52,6 @@
5052
#-----------------------------------------------------------------------------
5153
# Code
5254
#-----------------------------------------------------------------------------
53-
access_log = log.access_log
54-
app_log = log.app_log
5555

5656
here = os.path.dirname(__file__)
5757
pjoin = os.path.join
@@ -79,6 +79,17 @@ class NBViewer(Application):
7979

8080
name = Unicode('nbviewer')
8181

82+
aliases = Dict({
83+
'log-level' : 'Application.log_level',
84+
})
85+
86+
flags = Dict({
87+
'debug' : (
88+
{'Application' : {'log_level' : logging.DEBUG}},
89+
"Set log-level to debug, for the most verbose logging."
90+
),
91+
})
92+
8293
# Use this to insert custom configuration of handlers for NBViewer extensions
8394
handler_settings = Dict().tag(config=True)
8495

@@ -97,20 +108,20 @@ class NBViewer(Application):
97108
client = Any().tag(config=True)
98109
@default('client')
99110
def _default_client(self):
100-
client = HTTPClientClass()
111+
client = HTTPClientClass(log=self.log)
101112
client.cache = self.cache
102113
return client
103114

104115
index = Any().tag(config=True)
105116
@default('index')
106117
def _load_index(self):
107118
if os.environ.get('NBINDEX_PORT'):
108-
log.app_log.info("Indexing notebooks")
119+
self.log.info("Indexing notebooks")
109120
tcp_index = os.environ.get('NBINDEX_PORT')
110121
index_url = tcp_index.split('tcp://')[1]
111122
index_host, index_port = index_url.split(":")
112123
else:
113-
log.app_log.info("Not indexing notebooks")
124+
self.log.info("Not indexing notebooks")
114125
indexer = NoSearch()
115126
return indexer
116127

@@ -149,7 +160,7 @@ def cache(self):
149160
tcp_memcache = os.environ.get('NBCACHE_PORT')
150161
memcache_urls = tcp_memcache.split('tcp://')[1]
151162
if options.no_cache:
152-
log.app_log.info("Not using cache")
163+
self.log.info("Not using cache")
153164
cache = MockCache()
154165
elif pylibmc and memcache_urls:
155166
# setup memcache
@@ -161,13 +172,13 @@ def cache(self):
161172
kwargs['binary'] = True
162173
kwargs['username'] = username
163174
kwargs['password'] = password
164-
log.app_log.info("Using SASL memcache")
175+
self.log.info("Using SASL memcache")
165176
else:
166-
log.app_log.info("Using plain memcache")
177+
self.log.info("Using plain memcache")
167178

168179
cache = AsyncMultipartMemcache(memcache_urls.split(','), **kwargs)
169180
else:
170-
log.app_log.info("Using in-memory cache")
181+
self.log.info("Using in-memory cache")
171182
cache = DummyAsyncCache()
172183

173184
return cache
@@ -179,7 +190,7 @@ def env(self):
179190
try:
180191
git_data = git_info(here)
181192
except Exception as e:
182-
app_log.error("Failed to get git info: %s", e)
193+
self.log.error("Failed to get git info: %s", e)
183194
git_data = {}
184195
else:
185196
git_data['msg'] = escape(git_data['msg'])
@@ -196,18 +207,18 @@ def fetch_kwargs(self):
196207
fetch_kwargs = dict(connect_timeout=10,)
197208
if options.proxy_host:
198209
fetch_kwargs.update(proxy_host=options.proxy_host, proxy_port=options.proxy_port)
199-
log.app_log.info("Using web proxy {proxy_host}:{proxy_port}."
210+
self.log.info("Using web proxy {proxy_host}:{proxy_port}."
200211
"".format(**fetch_kwargs))
201212

202213
if options.no_check_certificate:
203214
fetch_kwargs.update(validate_cert=False)
204-
log.app_log.info("Not validating SSL certificates")
215+
self.log.info("Not validating SSL certificates")
205216

206217
return fetch_kwargs
207218

208219
@cached_property
209220
def formats(self):
210-
return self.configure_formats(log.app_log)
221+
return self.configure_formats()
211222

212223
# load frontpage sections
213224
@cached_property
@@ -223,6 +234,29 @@ def frontpage_setup(self):
223234
}
224235
return frontpage_setup
225236

237+
# Attribute inherited from traitlets.config.Application, automatically used to style logs
238+
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L191
239+
_log_formatter_cls = LogFormatter
240+
# Need Tornado LogFormatter for color logs, keys 'color' and 'end_color' in log_format
241+
242+
# Observed traitlet inherited again from traitlets.config.Application
243+
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L177
244+
@default('log_level')
245+
def _log_level_default(self):
246+
return logging.INFO
247+
248+
# Ditto the above: https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L197
249+
@default('log_format')
250+
def _log_format_default(self):
251+
"""override default log format to include time and color, plus always display the log level, not just when it's high"""
252+
return "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s %(module)s:%(lineno)d]%(end_color)s %(message)s"
253+
254+
# For consistency with JupyterHub logs
255+
@default('log_datefmt')
256+
def _log_datefmt_default(self):
257+
"""Exclude date from default date format"""
258+
return "%Y-%m-%d %H:%M:%S"
259+
226260
@cached_property
227261
def pool(self):
228262
if self.processes:
@@ -240,23 +274,25 @@ def rate_limiter(self):
240274
def static_paths(self):
241275
default_static_path = pjoin(here, 'static')
242276
if self.static_path:
243-
log.app_log.info("Using custom static path {}".format(self.static_path))
277+
self.log.info("Using custom static path {}".format(self.static_path))
244278
static_paths = [self.static_path, default_static_path]
245279
else:
246280
static_paths = [default_static_path]
281+
247282
return static_paths
248283

249284
@cached_property
250285
def template_paths(self):
251286
default_template_path = pjoin(here, 'templates')
252287
if options.template_path is not None:
253-
log.app_log.info("Using custom template path {}".format(options.template_path))
288+
self.log.info("Using custom template path {}".format(options.template_path))
254289
template_paths = [options.template_path, default_template_path]
255290
else:
256291
template_paths = [default_template_path]
292+
257293
return template_paths
258294

259-
def configure_formats(self, log, formats=None):
295+
def configure_formats(self, formats=None):
260296
"""
261297
Format-specific configuration.
262298
"""
@@ -277,8 +313,8 @@ def configure_formats(self, log, formats=None):
277313
# can't pickle exporter instances,
278314
formats[key]["exporter"] = exporter_cls
279315
else:
280-
formats[key]["exporter"] = exporter_cls(config=self.config, log=log)
281-
316+
formats[key]["exporter"] = exporter_cls(config=self.config, log=self.log)
317+
282318
return formats
283319

284320
def init_tornado_application(self):
@@ -305,8 +341,7 @@ def init_tornado_application(self):
305341

306342
# DEBUG env implies both autoreload and log-level
307343
if os.environ.get("DEBUG"):
308-
options.debug = True
309-
logging.getLogger().setLevel(logging.DEBUG)
344+
self.log.setLevel(logging.DEBUG)
310345

311346
# input traitlets to settings
312347
settings = dict(
@@ -337,6 +372,7 @@ def init_tornado_application(self):
337372
localfile_any_user=options.localfile_any_user,
338373
localfile_follow_symlinks=options.localfile_follow_symlinks,
339374
localfile_path=os.path.abspath(options.localfiles),
375+
log=self.log,
340376
log_function=log_request,
341377
mathjax_url=options.mathjax_url,
342378
max_cache_uris=self.max_cache_uris,
@@ -355,10 +391,35 @@ def init_tornado_application(self):
355391
)
356392

357393
if options.localfiles:
358-
log.app_log.warning("Serving local notebooks in %s, this can be a security risk", options.localfiles)
359-
394+
self.log.warning("Serving local notebooks in %s, this can be a security risk", options.localfiles)
395+
360396
# create the app
361-
self.tornado_application = web.Application(handlers, debug=options.debug, **settings)
397+
self.tornado_application = web.Application(handlers, **settings)
398+
399+
def init_logging(self):
400+
401+
# Note that we inherit a self.log attribute from traitlets.config.Application
402+
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L209
403+
# as well as a log_level attribute
404+
# https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L177
405+
406+
# This prevents double log messages because tornado use a root logger that
407+
# self.log is a child of. The logging module dispatches log messages to a log
408+
# and all of its ancestors until propagate is set to False
409+
self.log.propagate = False
410+
411+
tornado_log = logging.getLogger('tornado')
412+
# hook up tornado's loggers to our app handlers
413+
for log in (app_log, access_log, tornado_log, curl_log):
414+
# ensure all log statements identify the application they come from
415+
log.name = self.log.name
416+
log.parent = self.log
417+
log.propagate = True
418+
log.setLevel(self.log_level)
419+
420+
# disable curl debug, which logs all headers, info for upstream requests, which is TOO MUCH
421+
curl_log.setLevel(
422+
max(self.log_level, logging.INFO))
362423

363424
# Mostly copied from JupyterHub because if it isn't broken then don't fix it
364425
def write_config_file(self):
@@ -401,6 +462,7 @@ def __init__(self, *args, **kwargs):
401462

402463
# Inherited method from traitlets.Application
403464
self.load_config_file(options.config_file)
465+
self.init_logging()
404466
self.init_tornado_application()
405467

406468
def init_options():
@@ -426,7 +488,7 @@ def default_endpoint():
426488
define("cache_expiry_min", default=10*60, help="minimum cache expiry (seconds)", type=int)
427489
define("config_file", default='nbviewer_config.py', help="The config file to load", type=str)
428490
define("content_security_policy", default="connect-src 'none';", help="Content-Security-Policy header setting", type=str)
429-
define("debug", default=False, help="run in debug mode", type=bool)
491+
# define("debug", default=False, help="run in debug mode", type=bool)
430492
define("default_format", default="html", help="format to use for legacy / URLs", type=str)
431493
define("frontpage", default=FRONTPAGE_JSON, help="path to json file containing frontpage content", type=str)
432494
define("generate_config", default=False, help="Generate default config file and then stop.", type=bool)
@@ -462,16 +524,6 @@ def main(argv=None):
462524
init_options()
463525
tornado.options.parse_command_line(argv)
464526

465-
try:
466-
from tornado.curl_httpclient import curl_log
467-
except ImportError as e:
468-
log.app_log.warning("Failed to import curl: %s", e)
469-
else:
470-
# debug-level curl_log logs all headers, info for upstream requests,
471-
# which is just too much.
472-
curl_log.setLevel(max(log.app_log.getEffectiveLevel(), logging.INFO))
473-
474-
475527
# create and start the app
476528
nbviewer = NBViewer()
477529
app = nbviewer.tornado_application
@@ -485,7 +537,7 @@ def main(argv=None):
485537
}
486538

487539
http_server = httpserver.HTTPServer(app, xheaders=True, ssl_options=ssl_options)
488-
log.app_log.info("Listening on %s:%i, path %s", options.host, options.port,
540+
nbviewer.log.info("Listening on %s:%i, path %s", options.host, options.port,
489541
app.settings['base_url'])
490542
http_server.listen(options.port, options.host)
491543
ioloop.IOLoop.current().start()

nbviewer/handlers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
# the file COPYING, distributed as part of this software.
66
#-----------------------------------------------------------------------------
77
from tornado import web
8-
from tornado.log import app_log
98

109
from .utils import transform_ipynb_uri, url_path_join
1110

@@ -62,7 +61,7 @@ class CreateHandler(BaseHandler):
6261
def post(self):
6362
value = self.get_argument('gistnorurl', '')
6463
redirect_url = transform_ipynb_uri(value, self.get_provider_rewrites())
65-
app_log.info("create %s => %s", value, redirect_url)
64+
self.log.info("create %s => %s", value, redirect_url)
6665
self.redirect(url_path_join(self.base_url, redirect_url))
6766

6867
def get_provider_rewrites(self):

0 commit comments

Comments
 (0)