2015-08-02 18:59:11 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
#
|
|
|
|
# Copyright 2012 Facebook
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
|
|
# not use this file except in compliance with the License. You may obtain
|
|
|
|
# a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
# License for the specific language governing permissions and limitations
|
|
|
|
# under the License.
|
|
|
|
"""Logging support for Tornado.
|
|
|
|
|
|
|
|
Tornado uses three logger streams:
|
|
|
|
|
|
|
|
* ``tornado.access``: Per-request logging for Tornado's HTTP servers (and
|
|
|
|
potentially other servers in the future)
|
|
|
|
* ``tornado.application``: Logging of errors from application code (i.e.
|
|
|
|
uncaught exceptions from callbacks)
|
|
|
|
* ``tornado.general``: General-purpose logging, including any errors
|
|
|
|
or warnings from Tornado itself.
|
|
|
|
|
|
|
|
These streams may be configured independently using the standard library's
|
|
|
|
`logging` module. For example, you may wish to send ``tornado.access`` logs
|
|
|
|
to a separate file for analysis.
|
|
|
|
"""
|
|
|
|
from __future__ import absolute_import, division, print_function, with_statement
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import logging.handlers
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from tornado.escape import _unicode
|
|
|
|
from tornado.util import unicode_type, basestring_type
|
|
|
|
|
|
|
|
try:
|
|
|
|
import curses
|
|
|
|
except ImportError:
|
|
|
|
curses = None
|
|
|
|
|
|
|
|
# Logger objects for internal tornado use
|
|
|
|
access_log = logging.getLogger("tornado.access")
|
|
|
|
app_log = logging.getLogger("tornado.application")
|
|
|
|
gen_log = logging.getLogger("tornado.general")
|
|
|
|
|
|
|
|
|
|
|
|
def _stderr_supports_color():
|
|
|
|
color = False
|
2016-11-09 18:24:33 +00:00
|
|
|
if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty():
|
2015-08-02 18:59:11 +00:00
|
|
|
try:
|
|
|
|
curses.setupterm()
|
|
|
|
if curses.tigetnum("colors") > 0:
|
|
|
|
color = True
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
return color
|
|
|
|
|
|
|
|
|
2016-11-09 18:24:33 +00:00
|
|
|
def _safe_unicode(s):
|
|
|
|
try:
|
|
|
|
return _unicode(s)
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
return repr(s)
|
|
|
|
|
|
|
|
|
2015-08-02 18:59:11 +00:00
|
|
|
class LogFormatter(logging.Formatter):
|
|
|
|
"""Log formatter used in Tornado.
|
|
|
|
|
|
|
|
Key features of this formatter are:
|
|
|
|
|
|
|
|
* Color support when logging to a terminal that supports it.
|
|
|
|
* Timestamps on every log line.
|
|
|
|
* Robust against str/bytes encoding problems.
|
|
|
|
|
|
|
|
This formatter is enabled automatically by
|
|
|
|
`tornado.options.parse_command_line` (unless ``--logging=none`` is
|
|
|
|
used).
|
|
|
|
"""
|
2016-11-09 18:24:33 +00:00
|
|
|
DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s'
|
|
|
|
DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S'
|
|
|
|
DEFAULT_COLORS = {
|
|
|
|
logging.DEBUG: 4, # Blue
|
|
|
|
logging.INFO: 2, # Green
|
|
|
|
logging.WARNING: 3, # Yellow
|
|
|
|
logging.ERROR: 1, # Red
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, color=True, fmt=DEFAULT_FORMAT,
|
|
|
|
datefmt=DEFAULT_DATE_FORMAT, colors=DEFAULT_COLORS):
|
|
|
|
r"""
|
|
|
|
:arg bool color: Enables color support.
|
|
|
|
:arg string fmt: Log message format.
|
|
|
|
It will be applied to the attributes dict of log records. The
|
|
|
|
text between ``%(color)s`` and ``%(end_color)s`` will be colored
|
|
|
|
depending on the level if color support is on.
|
|
|
|
:arg dict colors: color mappings from logging level to terminal color
|
|
|
|
code
|
|
|
|
:arg string datefmt: Datetime format.
|
|
|
|
Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``.
|
|
|
|
|
|
|
|
.. versionchanged:: 3.2
|
|
|
|
|
|
|
|
Added ``fmt`` and ``datefmt`` arguments.
|
|
|
|
"""
|
|
|
|
logging.Formatter.__init__(self, datefmt=datefmt)
|
|
|
|
self._fmt = fmt
|
|
|
|
|
|
|
|
self._colors = {}
|
|
|
|
if color and _stderr_supports_color():
|
2015-08-02 18:59:11 +00:00
|
|
|
# The curses module has some str/bytes confusion in
|
|
|
|
# python3. Until version 3.2.3, most methods return
|
|
|
|
# bytes, but only accept strings. In addition, we want to
|
|
|
|
# output these strings with the logging module, which
|
|
|
|
# works with unicode strings. The explicit calls to
|
|
|
|
# unicode() below are harmless in python2 but will do the
|
|
|
|
# right conversion in python 3.
|
|
|
|
fg_color = (curses.tigetstr("setaf") or
|
|
|
|
curses.tigetstr("setf") or "")
|
|
|
|
if (3, 0) < sys.version_info < (3, 2, 3):
|
|
|
|
fg_color = unicode_type(fg_color, "ascii")
|
2016-11-09 18:24:33 +00:00
|
|
|
|
|
|
|
for levelno, code in colors.items():
|
|
|
|
self._colors[levelno] = unicode_type(curses.tparm(fg_color, code), "ascii")
|
2015-08-02 18:59:11 +00:00
|
|
|
self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii")
|
2016-11-09 18:24:33 +00:00
|
|
|
else:
|
|
|
|
self._normal = ''
|
2015-08-02 18:59:11 +00:00
|
|
|
|
|
|
|
def format(self, record):
|
|
|
|
try:
|
2016-11-09 18:24:33 +00:00
|
|
|
message = record.getMessage()
|
|
|
|
assert isinstance(message, basestring_type) # guaranteed by logging
|
|
|
|
# Encoding notes: The logging module prefers to work with character
|
|
|
|
# strings, but only enforces that log messages are instances of
|
|
|
|
# basestring. In python 2, non-ascii bytestrings will make
|
|
|
|
# their way through the logging framework until they blow up with
|
|
|
|
# an unhelpful decoding error (with this formatter it happens
|
|
|
|
# when we attach the prefix, but there are other opportunities for
|
|
|
|
# exceptions further along in the framework).
|
|
|
|
#
|
|
|
|
# If a byte string makes it this far, convert it to unicode to
|
|
|
|
# ensure it will make it out to the logs. Use repr() as a fallback
|
|
|
|
# to ensure that all byte strings can be converted successfully,
|
|
|
|
# but don't do it by default so we don't add extra quotes to ascii
|
|
|
|
# bytestrings. This is a bit of a hacky place to do this, but
|
|
|
|
# it's worth it since the encoding errors that would otherwise
|
|
|
|
# result are so useless (and tornado is fond of using utf8-encoded
|
|
|
|
# byte strings whereever possible).
|
|
|
|
record.message = _safe_unicode(message)
|
2015-08-02 18:59:11 +00:00
|
|
|
except Exception as e:
|
|
|
|
record.message = "Bad message (%r): %r" % (e, record.__dict__)
|
2016-11-09 18:24:33 +00:00
|
|
|
|
|
|
|
record.asctime = self.formatTime(record, self.datefmt)
|
|
|
|
|
|
|
|
if record.levelno in self._colors:
|
|
|
|
record.color = self._colors[record.levelno]
|
|
|
|
record.end_color = self._normal
|
|
|
|
else:
|
|
|
|
record.color = record.end_color = ''
|
|
|
|
|
|
|
|
formatted = self._fmt % record.__dict__
|
|
|
|
|
2015-08-02 18:59:11 +00:00
|
|
|
if record.exc_info:
|
|
|
|
if not record.exc_text:
|
|
|
|
record.exc_text = self.formatException(record.exc_info)
|
|
|
|
if record.exc_text:
|
2016-11-09 18:24:33 +00:00
|
|
|
# exc_text contains multiple lines. We need to _safe_unicode
|
2015-08-02 18:59:11 +00:00
|
|
|
# each line separately so that non-utf8 bytes don't cause
|
|
|
|
# all the newlines to turn into '\n'.
|
|
|
|
lines = [formatted.rstrip()]
|
2016-11-09 18:24:33 +00:00
|
|
|
lines.extend(_safe_unicode(ln) for ln in record.exc_text.split('\n'))
|
2015-08-02 18:59:11 +00:00
|
|
|
formatted = '\n'.join(lines)
|
|
|
|
return formatted.replace("\n", "\n ")
|
|
|
|
|
|
|
|
|
|
|
|
def enable_pretty_logging(options=None, logger=None):
|
|
|
|
"""Turns on formatted logging output as configured.
|
|
|
|
|
2016-11-09 18:24:33 +00:00
|
|
|
This is called automatically by `tornado.options.parse_command_line`
|
2015-08-02 18:59:11 +00:00
|
|
|
and `tornado.options.parse_config_file`.
|
|
|
|
"""
|
|
|
|
if options is None:
|
|
|
|
from tornado.options import options
|
2016-11-09 18:24:33 +00:00
|
|
|
if options.logging is None or options.logging.lower() == 'none':
|
2015-08-02 18:59:11 +00:00
|
|
|
return
|
|
|
|
if logger is None:
|
|
|
|
logger = logging.getLogger()
|
|
|
|
logger.setLevel(getattr(logging, options.logging.upper()))
|
|
|
|
if options.log_file_prefix:
|
|
|
|
channel = logging.handlers.RotatingFileHandler(
|
|
|
|
filename=options.log_file_prefix,
|
|
|
|
maxBytes=options.log_file_max_size,
|
|
|
|
backupCount=options.log_file_num_backups)
|
|
|
|
channel.setFormatter(LogFormatter(color=False))
|
|
|
|
logger.addHandler(channel)
|
|
|
|
|
|
|
|
if (options.log_to_stderr or
|
|
|
|
(options.log_to_stderr is None and not logger.handlers)):
|
|
|
|
# Set up color if we are in a tty and curses is installed
|
|
|
|
channel = logging.StreamHandler()
|
|
|
|
channel.setFormatter(LogFormatter())
|
|
|
|
logger.addHandler(channel)
|
|
|
|
|
|
|
|
|
|
|
|
def define_logging_options(options=None):
|
|
|
|
if options is None:
|
|
|
|
# late import to prevent cycle
|
|
|
|
from tornado.options import options
|
|
|
|
options.define("logging", default="info",
|
|
|
|
help=("Set the Python log level. If 'none', tornado won't touch the "
|
|
|
|
"logging configuration."),
|
|
|
|
metavar="debug|info|warning|error|none")
|
|
|
|
options.define("log_to_stderr", type=bool, default=None,
|
|
|
|
help=("Send log output to stderr (colorized if possible). "
|
|
|
|
"By default use stderr if --log_file_prefix is not set and "
|
|
|
|
"no other logging is configured."))
|
|
|
|
options.define("log_file_prefix", type=str, default=None, metavar="PATH",
|
|
|
|
help=("Path prefix for log files. "
|
|
|
|
"Note that if you are running multiple tornado processes, "
|
|
|
|
"log_file_prefix must be different for each of them (e.g. "
|
|
|
|
"include the port number)"))
|
|
|
|
options.define("log_file_max_size", type=int, default=100 * 1000 * 1000,
|
|
|
|
help="max size of log files before rollover")
|
|
|
|
options.define("log_file_num_backups", type=int, default=10,
|
|
|
|
help="number of log files to keep")
|
|
|
|
|
|
|
|
options.add_parse_callback(enable_pretty_logging)
|