log.py 12 KB

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