log.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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, Optional
  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. logging.CRITICAL: 5, # Magenta
  93. }
  94. def __init__(
  95. self,
  96. fmt: str = DEFAULT_FORMAT,
  97. datefmt: str = DEFAULT_DATE_FORMAT,
  98. style: str = "%",
  99. color: bool = True,
  100. colors: Dict[int, int] = DEFAULT_COLORS,
  101. ) -> None:
  102. r"""
  103. :arg bool color: Enables color support.
  104. :arg str fmt: Log message format.
  105. It will be applied to the attributes dict of log records. The
  106. text between ``%(color)s`` and ``%(end_color)s`` will be colored
  107. depending on the level if color support is on.
  108. :arg dict colors: color mappings from logging level to terminal color
  109. code
  110. :arg str datefmt: Datetime format.
  111. Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``.
  112. .. versionchanged:: 3.2
  113. Added ``fmt`` and ``datefmt`` arguments.
  114. """
  115. logging.Formatter.__init__(self, datefmt=datefmt)
  116. self._fmt = fmt
  117. self._colors = {} # type: Dict[int, str]
  118. if color and _stderr_supports_color():
  119. if curses is not None:
  120. fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or b""
  121. for levelno, code in colors.items():
  122. # Convert the terminal control characters from
  123. # bytes to unicode strings for easier use with the
  124. # logging module.
  125. self._colors[levelno] = unicode_type(
  126. curses.tparm(fg_color, code), "ascii"
  127. )
  128. normal = curses.tigetstr("sgr0")
  129. if normal is not None:
  130. self._normal = unicode_type(normal, "ascii")
  131. else:
  132. self._normal = ""
  133. else:
  134. # If curses is not present (currently we'll only get here for
  135. # colorama on windows), assume hard-coded ANSI color codes.
  136. for levelno, code in colors.items():
  137. self._colors[levelno] = "\033[2;3%dm" % code
  138. self._normal = "\033[0m"
  139. else:
  140. self._normal = ""
  141. def format(self, record: Any) -> str:
  142. try:
  143. message = record.getMessage()
  144. assert isinstance(message, basestring_type) # guaranteed by logging
  145. # Encoding notes: The logging module prefers to work with character
  146. # strings, but only enforces that log messages are instances of
  147. # basestring. In python 2, non-ascii bytestrings will make
  148. # their way through the logging framework until they blow up with
  149. # an unhelpful decoding error (with this formatter it happens
  150. # when we attach the prefix, but there are other opportunities for
  151. # exceptions further along in the framework).
  152. #
  153. # If a byte string makes it this far, convert it to unicode to
  154. # ensure it will make it out to the logs. Use repr() as a fallback
  155. # to ensure that all byte strings can be converted successfully,
  156. # but don't do it by default so we don't add extra quotes to ascii
  157. # bytestrings. This is a bit of a hacky place to do this, but
  158. # it's worth it since the encoding errors that would otherwise
  159. # result are so useless (and tornado is fond of using utf8-encoded
  160. # byte strings wherever possible).
  161. record.message = _safe_unicode(message)
  162. except Exception as e:
  163. record.message = f"Bad message ({e!r}): {record.__dict__!r}"
  164. record.asctime = self.formatTime(record, cast(str, self.datefmt))
  165. if record.levelno in self._colors:
  166. record.color = self._colors[record.levelno]
  167. record.end_color = self._normal
  168. else:
  169. record.color = record.end_color = ""
  170. formatted = self._fmt % record.__dict__
  171. if record.exc_info:
  172. if not record.exc_text:
  173. record.exc_text = self.formatException(record.exc_info)
  174. if record.exc_text:
  175. # exc_text contains multiple lines. We need to _safe_unicode
  176. # each line separately so that non-utf8 bytes don't cause
  177. # all the newlines to turn into '\n'.
  178. lines = [formatted.rstrip()]
  179. lines.extend(_safe_unicode(ln) for ln in record.exc_text.split("\n"))
  180. formatted = "\n".join(lines)
  181. return formatted.replace("\n", "\n ")
  182. def enable_pretty_logging(
  183. options: Any = None, logger: Optional[logging.Logger] = None
  184. ) -> None:
  185. """Turns on formatted logging output as configured.
  186. This is called automatically by `tornado.options.parse_command_line`
  187. and `tornado.options.parse_config_file`.
  188. """
  189. if options is None:
  190. import tornado.options
  191. options = tornado.options.options
  192. if options.logging is None or options.logging.lower() == "none":
  193. return
  194. if logger is None:
  195. logger = logging.getLogger()
  196. logger.setLevel(getattr(logging, options.logging.upper()))
  197. if options.log_file_prefix:
  198. rotate_mode = options.log_rotate_mode
  199. if rotate_mode == "size":
  200. channel = logging.handlers.RotatingFileHandler(
  201. filename=options.log_file_prefix,
  202. maxBytes=options.log_file_max_size,
  203. backupCount=options.log_file_num_backups,
  204. encoding="utf-8",
  205. ) # type: logging.Handler
  206. elif rotate_mode == "time":
  207. channel = logging.handlers.TimedRotatingFileHandler(
  208. filename=options.log_file_prefix,
  209. when=options.log_rotate_when,
  210. interval=options.log_rotate_interval,
  211. backupCount=options.log_file_num_backups,
  212. encoding="utf-8",
  213. )
  214. else:
  215. error_message = (
  216. "The value of log_rotate_mode option should be "
  217. + '"size" or "time", not "%s".' % rotate_mode
  218. )
  219. raise ValueError(error_message)
  220. channel.setFormatter(LogFormatter(color=False))
  221. logger.addHandler(channel)
  222. if options.log_to_stderr or (options.log_to_stderr is None and not logger.handlers):
  223. # Set up color if we are in a tty and curses is installed
  224. channel = logging.StreamHandler()
  225. channel.setFormatter(LogFormatter())
  226. logger.addHandler(channel)
  227. def define_logging_options(options: Any = None) -> None:
  228. """Add logging-related flags to ``options``.
  229. These options are present automatically on the default options instance;
  230. this method is only necessary if you have created your own `.OptionParser`.
  231. .. versionadded:: 4.2
  232. This function existed in prior versions but was broken and undocumented until 4.2.
  233. """
  234. if options is None:
  235. # late import to prevent cycle
  236. import tornado.options
  237. options = tornado.options.options
  238. options.define(
  239. "logging",
  240. default="info",
  241. help=(
  242. "Set the Python log level. If 'none', tornado won't touch the "
  243. "logging configuration."
  244. ),
  245. metavar="debug|info|warning|error|none",
  246. )
  247. options.define(
  248. "log_to_stderr",
  249. type=bool,
  250. default=None,
  251. help=(
  252. "Send log output to stderr (colorized if possible). "
  253. "By default use stderr if --log_file_prefix is not set and "
  254. "no other logging is configured."
  255. ),
  256. )
  257. options.define(
  258. "log_file_prefix",
  259. type=str,
  260. default=None,
  261. metavar="PATH",
  262. help=(
  263. "Path prefix for log files. "
  264. "Note that if you are running multiple tornado processes, "
  265. "log_file_prefix must be different for each of them (e.g. "
  266. "include the port number)"
  267. ),
  268. )
  269. options.define(
  270. "log_file_max_size",
  271. type=int,
  272. default=100 * 1000 * 1000,
  273. help="max size of log files before rollover",
  274. )
  275. options.define(
  276. "log_file_num_backups", type=int, default=10, help="number of log files to keep"
  277. )
  278. options.define(
  279. "log_rotate_when",
  280. type=str,
  281. default="midnight",
  282. help=(
  283. "specify the type of TimedRotatingFileHandler interval "
  284. "other options:('S', 'M', 'H', 'D', 'W0'-'W6')"
  285. ),
  286. )
  287. options.define(
  288. "log_rotate_interval",
  289. type=int,
  290. default=1,
  291. help="The interval value of timed rotating",
  292. )
  293. options.define(
  294. "log_rotate_mode",
  295. type=str,
  296. default="size",
  297. help="The mode of rotating files(time or size)",
  298. )
  299. options.add_parse_callback(lambda: enable_pretty_logging(options))