Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tornado/log.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

115 statements  

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 

17Tornado uses three logger streams: 

18 

19* ``tornado.access``: Per-request logging for Tornado's HTTP servers (and 

20 potentially other servers in the future) 

21* ``tornado.application``: Logging of errors from application code (i.e. 

22 uncaught exceptions from callbacks) 

23* ``tornado.general``: General-purpose logging, including any errors 

24 or warnings from Tornado itself. 

25 

26These streams may be configured independently using the standard library's 

27`logging` module. For example, you may wish to send ``tornado.access`` logs 

28to a separate file for analysis. 

29""" 

30 

31import logging 

32import logging.handlers 

33import sys 

34 

35from tornado.escape import _unicode 

36from tornado.util import basestring_type, unicode_type 

37 

38try: 

39 import colorama # type: ignore 

40except ImportError: 

41 colorama = None 

42 

43try: 

44 import curses 

45except ImportError: 

46 curses = None # type: ignore 

47 

48from typing import Any, cast 

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() -> bool: 

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( 

65 colorama.initialise, "wrapped_stderr", object() 

66 ): 

67 return True 

68 except Exception: 

69 # Very broad exception handling because it's always better to 

70 # fall back to non-colored logs than to break at startup. 

71 pass 

72 return False 

73 

74 

75def _safe_unicode(s: Any) -> str: 

76 try: 

77 return _unicode(s) 

78 except UnicodeDecodeError: 

79 return repr(s) 

80 

81 

82class LogFormatter(logging.Formatter): 

83 """Log formatter used in Tornado. 

84 

85 Key features of this formatter are: 

86 

87 * Color support when logging to a terminal that supports it. 

88 * Timestamps on every log line. 

89 * Robust against str/bytes encoding problems. 

90 

91 This formatter is enabled automatically by 

92 `tornado.options.parse_command_line` or `tornado.options.parse_config_file` 

93 (unless ``--logging=none`` is used). 

94 

95 Color support on Windows versions that do not support ANSI color codes is 

96 enabled by use of the colorama__ library. Applications that wish to use 

97 this must first initialize colorama with a call to ``colorama.init``. 

98 See the colorama documentation for details. 

99 

100 __ https://pypi.python.org/pypi/colorama 

101 

102 .. versionchanged:: 4.5 

103 Added support for ``colorama``. Changed the constructor 

104 signature to be compatible with `logging.config.dictConfig`. 

105 """ 

106 

107 DEFAULT_FORMAT = "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s" # noqa: E501 

108 DEFAULT_DATE_FORMAT = "%y%m%d %H:%M:%S" 

109 DEFAULT_COLORS = { 

110 logging.DEBUG: 4, # Blue 

111 logging.INFO: 2, # Green 

112 logging.WARNING: 3, # Yellow 

113 logging.ERROR: 1, # Red 

114 logging.CRITICAL: 5, # Magenta 

115 } 

116 

117 def __init__( 

118 self, 

119 fmt: str = DEFAULT_FORMAT, 

120 datefmt: str = DEFAULT_DATE_FORMAT, 

121 style: str = "%", 

122 color: bool = True, 

123 colors: dict[int, int] = DEFAULT_COLORS, 

124 ) -> None: 

125 r""" 

126 :arg bool color: Enables color support. 

127 :arg str fmt: Log message format. 

128 It will be applied to the attributes dict of log records. The 

129 text between ``%(color)s`` and ``%(end_color)s`` will be colored 

130 depending on the level if color support is on. 

131 :arg dict colors: color mappings from logging level to terminal color 

132 code 

133 :arg str datefmt: Datetime format. 

134 Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``. 

135 

136 .. versionchanged:: 3.2 

137 

138 Added ``fmt`` and ``datefmt`` arguments. 

139 """ 

140 logging.Formatter.__init__(self, datefmt=datefmt) 

141 self._fmt = fmt 

142 

143 self._colors: dict[int, str] = {} 

144 if color and _stderr_supports_color(): 

145 if curses is not None: 

146 fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or b"" 

147 

148 for levelno, code in colors.items(): 

149 # Convert the terminal control characters from 

150 # bytes to unicode strings for easier use with the 

151 # logging module. 

152 self._colors[levelno] = unicode_type( 

153 curses.tparm(fg_color, code), "ascii" 

154 ) 

155 normal = curses.tigetstr("sgr0") 

156 if normal is not None: 

157 self._normal = unicode_type(normal, "ascii") 

158 else: 

159 self._normal = "" 

160 else: 

161 # If curses is not present (currently we'll only get here for 

162 # colorama on windows), assume hard-coded ANSI color codes. 

163 for levelno, code in colors.items(): 

164 self._colors[levelno] = "\033[2;3%dm" % code 

165 self._normal = "\033[0m" 

166 else: 

167 self._normal = "" 

168 

169 def format(self, record: Any) -> str: 

170 try: 

171 message = record.getMessage() 

172 assert isinstance(message, basestring_type) # guaranteed by logging 

173 # Encoding notes: The logging module prefers to work with character 

174 # strings, but only enforces that log messages are instances of 

175 # basestring. In python 2, non-ascii bytestrings will make 

176 # their way through the logging framework until they blow up with 

177 # an unhelpful decoding error (with this formatter it happens 

178 # when we attach the prefix, but there are other opportunities for 

179 # exceptions further along in the framework). 

180 # 

181 # If a byte string makes it this far, convert it to unicode to 

182 # ensure it will make it out to the logs. Use repr() as a fallback 

183 # to ensure that all byte strings can be converted successfully, 

184 # but don't do it by default so we don't add extra quotes to ascii 

185 # bytestrings. This is a bit of a hacky place to do this, but 

186 # it's worth it since the encoding errors that would otherwise 

187 # result are so useless (and tornado is fond of using utf8-encoded 

188 # byte strings wherever possible). 

189 record.message = _safe_unicode(message) 

190 except Exception as e: 

191 record.message = f"Bad message ({e!r}): {record.__dict__!r}" 

192 

193 record.asctime = self.formatTime(record, cast(str, self.datefmt)) 

194 

195 if record.levelno in self._colors: 

196 record.color = self._colors[record.levelno] 

197 record.end_color = self._normal 

198 else: 

199 record.color = record.end_color = "" 

200 

201 formatted = self._fmt % record.__dict__ 

202 

203 if record.exc_info: 

204 if not record.exc_text: 

205 record.exc_text = self.formatException(record.exc_info) 

206 if record.exc_text: 

207 # exc_text contains multiple lines. We need to _safe_unicode 

208 # each line separately so that non-utf8 bytes don't cause 

209 # all the newlines to turn into '\n'. 

210 lines = [formatted.rstrip()] 

211 lines.extend(_safe_unicode(ln) for ln in record.exc_text.split("\n")) 

212 formatted = "\n".join(lines) 

213 return formatted.replace("\n", "\n ") 

214 

215 

216def enable_pretty_logging( 

217 options: Any = None, logger: logging.Logger | None = None 

218) -> None: 

219 """Turns on formatted logging output as configured. 

220 

221 This is called automatically by `tornado.options.parse_command_line` 

222 and `tornado.options.parse_config_file`. 

223 """ 

224 if options is None: 

225 import tornado.options 

226 

227 options = tornado.options.options 

228 if options.logging is None or options.logging.lower() == "none": 

229 return 

230 if logger is None: 

231 logger = logging.getLogger() 

232 logger.setLevel(getattr(logging, options.logging.upper())) 

233 if options.log_file_prefix: 

234 rotate_mode = options.log_rotate_mode 

235 if rotate_mode == "size": 

236 channel: logging.Handler = logging.handlers.RotatingFileHandler( 

237 filename=options.log_file_prefix, 

238 maxBytes=options.log_file_max_size, 

239 backupCount=options.log_file_num_backups, 

240 encoding="utf-8", 

241 ) 

242 elif rotate_mode == "time": 

243 channel = logging.handlers.TimedRotatingFileHandler( 

244 filename=options.log_file_prefix, 

245 when=options.log_rotate_when, 

246 interval=options.log_rotate_interval, 

247 backupCount=options.log_file_num_backups, 

248 encoding="utf-8", 

249 ) 

250 else: 

251 error_message = ( 

252 "The value of log_rotate_mode option should be " 

253 + '"size" or "time", not "%s".' % rotate_mode 

254 ) 

255 raise ValueError(error_message) 

256 channel.setFormatter(LogFormatter(color=False)) 

257 logger.addHandler(channel) 

258 

259 if options.log_to_stderr or (options.log_to_stderr is None and not logger.handlers): 

260 # Set up color if we are in a tty and curses is installed 

261 channel = logging.StreamHandler() 

262 channel.setFormatter(LogFormatter()) 

263 logger.addHandler(channel) 

264 

265 

266def define_logging_options(options: Any = None) -> None: 

267 """Add logging-related flags to ``options``. 

268 

269 These options are present automatically on the default options instance; 

270 this method is only necessary if you have created your own `.OptionParser`. 

271 

272 .. versionadded:: 4.2 

273 This function existed in prior versions but was broken and undocumented until 4.2. 

274 """ 

275 if options is None: 

276 # late import to prevent cycle 

277 import tornado.options 

278 

279 options = tornado.options.options 

280 options.define( 

281 "logging", 

282 default="info", 

283 help=( 

284 "Set the Python log level. If 'none', tornado won't touch the " 

285 "logging configuration." 

286 ), 

287 metavar="debug|info|warning|error|none", 

288 ) 

289 options.define( 

290 "log_to_stderr", 

291 type=bool, 

292 default=None, 

293 help=( 

294 "Send log output to stderr (colorized if possible). " 

295 "By default use stderr if --log_file_prefix is not set and " 

296 "no other logging is configured." 

297 ), 

298 ) 

299 options.define( 

300 "log_file_prefix", 

301 type=str, 

302 default=None, 

303 metavar="PATH", 

304 help=( 

305 "Path prefix for log files. " 

306 "Note that if you are running multiple tornado processes, " 

307 "log_file_prefix must be different for each of them (e.g. " 

308 "include the port number)" 

309 ), 

310 ) 

311 options.define( 

312 "log_file_max_size", 

313 type=int, 

314 default=100 * 1000 * 1000, 

315 help="max size of log files before rollover", 

316 ) 

317 options.define( 

318 "log_file_num_backups", type=int, default=10, help="number of log files to keep" 

319 ) 

320 

321 options.define( 

322 "log_rotate_when", 

323 type=str, 

324 default="midnight", 

325 help=( 

326 "specify the type of TimedRotatingFileHandler interval " 

327 "other options:('S', 'M', 'H', 'D', 'W0'-'W6')" 

328 ), 

329 ) 

330 options.define( 

331 "log_rotate_interval", 

332 type=int, 

333 default=1, 

334 help="The interval value of timed rotating", 

335 ) 

336 

337 options.define( 

338 "log_rotate_mode", 

339 type=str, 

340 default="size", 

341 help="The mode of rotating files(time or size)", 

342 ) 

343 

344 options.add_parse_callback(lambda: enable_pretty_logging(options))