1#!/usr/bin/env python
2#
3# Copyright 2012 Facebook
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16"""Logging support for Tornado.
17
18Tornado uses three logger streams:
19
20* ``tornado.access``: Per-request logging for Tornado's HTTP servers (and
21  potentially other servers in the future)
22* ``tornado.application``: Logging of errors from application code (i.e.
23  uncaught exceptions from callbacks)
24* ``tornado.general``: General-purpose logging, including any errors
25  or warnings from Tornado itself.
26
27These streams may be configured independently using the standard library's
28`logging` module.  For example, you may wish to send ``tornado.access`` logs
29to a separate file for analysis.
30"""
31from __future__ import absolute_import, division, print_function
32
33import logging
34import logging.handlers
35import sys
36
37from tornado.escape import _unicode
38from tornado.util import unicode_type, basestring_type
39
40try:
41    import colorama
42except ImportError:
43    colorama = None
44
45try:
46    import curses  # type: ignore
47except ImportError:
48    curses = None
49
50# Logger objects for internal tornado use
51access_log = logging.getLogger("tornado.access")
52app_log = logging.getLogger("tornado.application")
53gen_log = logging.getLogger("tornado.general")
54
55
56def _stderr_supports_color():
57    try:
58        if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty():
59            if curses:
60                curses.setupterm()
61                if curses.tigetnum("colors") > 0:
62                    return True
63            elif colorama:
64                if sys.stderr is getattr(colorama.initialise, 'wrapped_stderr',
65                                         object()):
66                    return True
67    except Exception:
68        # Very broad exception handling because it's always better to
69        # fall back to non-colored logs than to break at startup.
70        pass
71    return False
72
73
74def _safe_unicode(s):
75    try:
76        return _unicode(s)
77    except UnicodeDecodeError:
78        return repr(s)
79
80
81class LogFormatter(logging.Formatter):
82    """Log formatter used in Tornado.
83
84    Key features of this formatter are:
85
86    * Color support when logging to a terminal that supports it.
87    * Timestamps on every log line.
88    * Robust against str/bytes encoding problems.
89
90    This formatter is enabled automatically by
91    `tornado.options.parse_command_line` or `tornado.options.parse_config_file`
92    (unless ``--logging=none`` is used).
93
94    Color support on Windows versions that do not support ANSI color codes is
95    enabled by use of the colorama__ library. Applications that wish to use
96    this must first initialize colorama with a call to ``colorama.init``.
97    See the colorama documentation for details.
98
99    __ https://pypi.python.org/pypi/colorama
100
101    .. versionchanged:: 4.5
102       Added support for ``colorama``. Changed the constructor
103       signature to be compatible with `logging.config.dictConfig`.
104    """
105    DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s'
106    DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S'
107    DEFAULT_COLORS = {
108        logging.DEBUG: 4,  # Blue
109        logging.INFO: 2,  # Green
110        logging.WARNING: 3,  # Yellow
111        logging.ERROR: 1,  # Red
112    }
113
114    def __init__(self, fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT,
115                 style='%', color=True, colors=DEFAULT_COLORS):
116        r"""
117        :arg bool color: Enables color support.
118        :arg string fmt: Log message format.
119          It will be applied to the attributes dict of log records. The
120          text between ``%(color)s`` and ``%(end_color)s`` will be colored
121          depending on the level if color support is on.
122        :arg dict colors: color mappings from logging level to terminal color
123          code
124        :arg string datefmt: Datetime format.
125          Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``.
126
127        .. versionchanged:: 3.2
128
129           Added ``fmt`` and ``datefmt`` arguments.
130        """
131        logging.Formatter.__init__(self, datefmt=datefmt)
132        self._fmt = fmt
133
134        self._colors = {}
135        if color and _stderr_supports_color():
136            if curses is not None:
137                # The curses module has some str/bytes confusion in
138                # python3.  Until version 3.2.3, most methods return
139                # bytes, but only accept strings.  In addition, we want to
140                # output these strings with the logging module, which
141                # works with unicode strings.  The explicit calls to
142                # unicode() below are harmless in python2 but will do the
143                # right conversion in python 3.
144                fg_color = (curses.tigetstr("setaf") or
145                            curses.tigetstr("setf") or "")
146                if (3, 0) < sys.version_info < (3, 2, 3):
147                    fg_color = unicode_type(fg_color, "ascii")
148
149                for levelno, code in colors.items():
150                    self._colors[levelno] = unicode_type(curses.tparm(fg_color, code), "ascii")
151                self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii")
152            else:
153                # If curses is not present (currently we'll only get here for
154                # colorama on windows), assume hard-coded ANSI color codes.
155                for levelno, code in colors.items():
156                    self._colors[levelno] = '\033[2;3%dm' % code
157                self._normal = '\033[0m'
158        else:
159            self._normal = ''
160
161    def format(self, record):
162        try:
163            message = record.getMessage()
164            assert isinstance(message, basestring_type)  # guaranteed by logging
165            # Encoding notes:  The logging module prefers to work with character
166            # strings, but only enforces that log messages are instances of
167            # basestring.  In python 2, non-ascii bytestrings will make
168            # their way through the logging framework until they blow up with
169            # an unhelpful decoding error (with this formatter it happens
170            # when we attach the prefix, but there are other opportunities for
171            # exceptions further along in the framework).
172            #
173            # If a byte string makes it this far, convert it to unicode to
174            # ensure it will make it out to the logs.  Use repr() as a fallback
175            # to ensure that all byte strings can be converted successfully,
176            # but don't do it by default so we don't add extra quotes to ascii
177            # bytestrings.  This is a bit of a hacky place to do this, but
178            # it's worth it since the encoding errors that would otherwise
179            # result are so useless (and tornado is fond of using utf8-encoded
180            # byte strings whereever possible).
181            record.message = _safe_unicode(message)
182        except Exception as e:
183            record.message = "Bad message (%r): %r" % (e, record.__dict__)
184
185        record.asctime = self.formatTime(record, self.datefmt)
186
187        if record.levelno in self._colors:
188            record.color = self._colors[record.levelno]
189            record.end_color = self._normal
190        else:
191            record.color = record.end_color = ''
192
193        formatted = self._fmt % record.__dict__
194
195        if record.exc_info:
196            if not record.exc_text:
197                record.exc_text = self.formatException(record.exc_info)
198        if record.exc_text:
199            # exc_text contains multiple lines.  We need to _safe_unicode
200            # each line separately so that non-utf8 bytes don't cause
201            # all the newlines to turn into '\n'.
202            lines = [formatted.rstrip()]
203            lines.extend(_safe_unicode(ln) for ln in record.exc_text.split('\n'))
204            formatted = '\n'.join(lines)
205        return formatted.replace("\n", "\n    ")
206
207
208def enable_pretty_logging(options=None, logger=None):
209    """Turns on formatted logging output as configured.
210
211    This is called automatically by `tornado.options.parse_command_line`
212    and `tornado.options.parse_config_file`.
213    """
214    if options is None:
215        import tornado.options
216        options = tornado.options.options
217    if options.logging is None or options.logging.lower() == 'none':
218        return
219    if logger is None:
220        logger = logging.getLogger()
221    logger.setLevel(getattr(logging, options.logging.upper()))
222    if options.log_file_prefix:
223        rotate_mode = options.log_rotate_mode
224        if rotate_mode == 'size':
225            channel = logging.handlers.RotatingFileHandler(
226                filename=options.log_file_prefix,
227                maxBytes=options.log_file_max_size,
228                backupCount=options.log_file_num_backups)
229        elif rotate_mode == 'time':
230            channel = logging.handlers.TimedRotatingFileHandler(
231                filename=options.log_file_prefix,
232                when=options.log_rotate_when,
233                interval=options.log_rotate_interval,
234                backupCount=options.log_file_num_backups)
235        else:
236            error_message = 'The value of log_rotate_mode option should be ' +\
237                            '"size" or "time", not "%s".' % rotate_mode
238            raise ValueError(error_message)
239        channel.setFormatter(LogFormatter(color=False))
240        logger.addHandler(channel)
241
242    if (options.log_to_stderr or
243            (options.log_to_stderr is None and not logger.handlers)):
244        # Set up color if we are in a tty and curses is installed
245        channel = logging.StreamHandler()
246        channel.setFormatter(LogFormatter())
247        logger.addHandler(channel)
248
249
250def define_logging_options(options=None):
251    """Add logging-related flags to ``options``.
252
253    These options are present automatically on the default options instance;
254    this method is only necessary if you have created your own `.OptionParser`.
255
256    .. versionadded:: 4.2
257        This function existed in prior versions but was broken and undocumented until 4.2.
258    """
259    if options is None:
260        # late import to prevent cycle
261        import tornado.options
262        options = tornado.options.options
263    options.define("logging", default="info",
264                   help=("Set the Python log level. If 'none', tornado won't touch the "
265                         "logging configuration."),
266                   metavar="debug|info|warning|error|none")
267    options.define("log_to_stderr", type=bool, default=None,
268                   help=("Send log output to stderr (colorized if possible). "
269                         "By default use stderr if --log_file_prefix is not set and "
270                         "no other logging is configured."))
271    options.define("log_file_prefix", type=str, default=None, metavar="PATH",
272                   help=("Path prefix for log files. "
273                         "Note that if you are running multiple tornado processes, "
274                         "log_file_prefix must be different for each of them (e.g. "
275                         "include the port number)"))
276    options.define("log_file_max_size", type=int, default=100 * 1000 * 1000,
277                   help="max size of log files before rollover")
278    options.define("log_file_num_backups", type=int, default=10,
279                   help="number of log files to keep")
280
281    options.define("log_rotate_when", type=str, default='midnight',
282                   help=("specify the type of TimedRotatingFileHandler interval "
283                         "other options:('S', 'M', 'H', 'D', 'W0'-'W6')"))
284    options.define("log_rotate_interval", type=int, default=1,
285                   help="The interval value of timed rotating")
286
287    options.define("log_rotate_mode", type=str, default='size',
288                   help="The mode of rotating files(time or size)")
289
290    options.add_parse_callback(lambda: enable_pretty_logging(options))
291