| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- #
- # 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.
- """
- import logging
- import logging.handlers
- import sys
- from tornado.escape import _unicode
- from tornado.util import unicode_type, basestring_type
- try:
- import colorama # type: ignore
- except ImportError:
- colorama = None
- try:
- import curses
- except ImportError:
- curses = None # type: ignore
- from typing import Dict, Any, cast
- # 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() -> bool:
- try:
- if hasattr(sys.stderr, "isatty") and sys.stderr.isatty():
- if curses:
- curses.setupterm()
- if curses.tigetnum("colors") > 0:
- return True
- elif colorama:
- if sys.stderr is getattr(
- colorama.initialise, "wrapped_stderr", object()
- ):
- return True
- except Exception:
- # Very broad exception handling because it's always better to
- # fall back to non-colored logs than to break at startup.
- pass
- return False
- def _safe_unicode(s: Any) -> str:
- try:
- return _unicode(s)
- except UnicodeDecodeError:
- return repr(s)
- 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` or `tornado.options.parse_config_file`
- (unless ``--logging=none`` is used).
- Color support on Windows versions that do not support ANSI color codes is
- enabled by use of the colorama__ library. Applications that wish to use
- this must first initialize colorama with a call to ``colorama.init``.
- See the colorama documentation for details.
- __ https://pypi.python.org/pypi/colorama
- .. versionchanged:: 4.5
- Added support for ``colorama``. Changed the constructor
- signature to be compatible with `logging.config.dictConfig`.
- """
- DEFAULT_FORMAT = "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s" # noqa: E501
- 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,
- fmt: str = DEFAULT_FORMAT,
- datefmt: str = DEFAULT_DATE_FORMAT,
- style: str = "%",
- color: bool = True,
- colors: Dict[int, int] = DEFAULT_COLORS,
- ) -> None:
- r"""
- :arg bool color: Enables color support.
- :arg str 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 str 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 = {} # type: Dict[int, str]
- if color and _stderr_supports_color():
- if curses is not None:
- fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or b""
- for levelno, code in colors.items():
- # Convert the terminal control characters from
- # bytes to unicode strings for easier use with the
- # logging module.
- self._colors[levelno] = unicode_type(
- curses.tparm(fg_color, code), "ascii"
- )
- self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii")
- else:
- # If curses is not present (currently we'll only get here for
- # colorama on windows), assume hard-coded ANSI color codes.
- for levelno, code in colors.items():
- self._colors[levelno] = "\033[2;3%dm" % code
- self._normal = "\033[0m"
- else:
- self._normal = ""
- def format(self, record: Any) -> str:
- try:
- 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 wherever possible).
- record.message = _safe_unicode(message)
- except Exception as e:
- record.message = "Bad message (%r): %r" % (e, record.__dict__)
- record.asctime = self.formatTime(record, cast(str, 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__
- if record.exc_info:
- if not record.exc_text:
- record.exc_text = self.formatException(record.exc_info)
- if record.exc_text:
- # exc_text contains multiple lines. We need to _safe_unicode
- # each line separately so that non-utf8 bytes don't cause
- # all the newlines to turn into '\n'.
- lines = [formatted.rstrip()]
- lines.extend(_safe_unicode(ln) for ln in record.exc_text.split("\n"))
- formatted = "\n".join(lines)
- return formatted.replace("\n", "\n ")
- def enable_pretty_logging(options: Any = None, logger: logging.Logger = None) -> None:
- """Turns on formatted logging output as configured.
- This is called automatically by `tornado.options.parse_command_line`
- and `tornado.options.parse_config_file`.
- """
- if options is None:
- import tornado.options
- options = tornado.options.options
- if options.logging is None or options.logging.lower() == "none":
- return
- if logger is None:
- logger = logging.getLogger()
- logger.setLevel(getattr(logging, options.logging.upper()))
- if options.log_file_prefix:
- rotate_mode = options.log_rotate_mode
- if rotate_mode == "size":
- channel = logging.handlers.RotatingFileHandler(
- filename=options.log_file_prefix,
- maxBytes=options.log_file_max_size,
- backupCount=options.log_file_num_backups,
- encoding="utf-8",
- ) # type: logging.Handler
- elif rotate_mode == "time":
- channel = logging.handlers.TimedRotatingFileHandler(
- filename=options.log_file_prefix,
- when=options.log_rotate_when,
- interval=options.log_rotate_interval,
- backupCount=options.log_file_num_backups,
- encoding="utf-8",
- )
- else:
- error_message = (
- "The value of log_rotate_mode option should be "
- + '"size" or "time", not "%s".' % rotate_mode
- )
- raise ValueError(error_message)
- 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: Any = None) -> None:
- """Add logging-related flags to ``options``.
- These options are present automatically on the default options instance;
- this method is only necessary if you have created your own `.OptionParser`.
- .. versionadded:: 4.2
- This function existed in prior versions but was broken and undocumented until 4.2.
- """
- if options is None:
- # late import to prevent cycle
- import tornado.options
- options = tornado.options.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.define(
- "log_rotate_when",
- type=str,
- default="midnight",
- help=(
- "specify the type of TimedRotatingFileHandler interval "
- "other options:('S', 'M', 'H', 'D', 'W0'-'W6')"
- ),
- )
- options.define(
- "log_rotate_interval",
- type=int,
- default=1,
- help="The interval value of timed rotating",
- )
- options.define(
- "log_rotate_mode",
- type=str,
- default="size",
- help="The mode of rotating files(time or size)",
- )
- options.add_parse_callback(lambda: enable_pretty_logging(options))
|