logging.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. # -*- coding: utf-8 -*-
  2. """ Access and control log capturing. """
  3. from __future__ import absolute_import
  4. from __future__ import division
  5. from __future__ import print_function
  6. import logging
  7. import re
  8. from contextlib import contextmanager
  9. import py
  10. import six
  11. import pytest
  12. from _pytest.compat import dummy_context_manager
  13. from _pytest.config import create_terminal_writer
  14. from _pytest.pathlib import Path
  15. DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
  16. DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
  17. _ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
  18. def _remove_ansi_escape_sequences(text):
  19. return _ANSI_ESCAPE_SEQ.sub("", text)
  20. class ColoredLevelFormatter(logging.Formatter):
  21. """
  22. Colorize the %(levelname)..s part of the log format passed to __init__.
  23. """
  24. LOGLEVEL_COLOROPTS = {
  25. logging.CRITICAL: {"red"},
  26. logging.ERROR: {"red", "bold"},
  27. logging.WARNING: {"yellow"},
  28. logging.WARN: {"yellow"},
  29. logging.INFO: {"green"},
  30. logging.DEBUG: {"purple"},
  31. logging.NOTSET: set(),
  32. }
  33. LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-]?\d*s)")
  34. def __init__(self, terminalwriter, *args, **kwargs):
  35. super(ColoredLevelFormatter, self).__init__(*args, **kwargs)
  36. if six.PY2:
  37. self._original_fmt = self._fmt
  38. else:
  39. self._original_fmt = self._style._fmt
  40. self._level_to_fmt_mapping = {}
  41. levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
  42. if not levelname_fmt_match:
  43. return
  44. levelname_fmt = levelname_fmt_match.group()
  45. for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
  46. formatted_levelname = levelname_fmt % {
  47. "levelname": logging.getLevelName(level)
  48. }
  49. # add ANSI escape sequences around the formatted levelname
  50. color_kwargs = {name: True for name in color_opts}
  51. colorized_formatted_levelname = terminalwriter.markup(
  52. formatted_levelname, **color_kwargs
  53. )
  54. self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
  55. colorized_formatted_levelname, self._fmt
  56. )
  57. def format(self, record):
  58. fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt)
  59. if six.PY2:
  60. self._fmt = fmt
  61. else:
  62. self._style._fmt = fmt
  63. return super(ColoredLevelFormatter, self).format(record)
  64. if not six.PY2:
  65. # Formatter classes don't support format styles in PY2
  66. class PercentStyleMultiline(logging.PercentStyle):
  67. """A logging style with special support for multiline messages.
  68. If the message of a record consists of multiple lines, this style
  69. formats the message as if each line were logged separately.
  70. """
  71. @staticmethod
  72. def _update_message(record_dict, message):
  73. tmp = record_dict.copy()
  74. tmp["message"] = message
  75. return tmp
  76. def format(self, record):
  77. if "\n" in record.message:
  78. lines = record.message.splitlines()
  79. formatted = self._fmt % self._update_message(record.__dict__, lines[0])
  80. # TODO optimize this by introducing an option that tells the
  81. # logging framework that the indentation doesn't
  82. # change. This allows to compute the indentation only once.
  83. indentation = _remove_ansi_escape_sequences(formatted).find(lines[0])
  84. lines[0] = formatted
  85. return ("\n" + " " * indentation).join(lines)
  86. else:
  87. return self._fmt % record.__dict__
  88. def get_option_ini(config, *names):
  89. for name in names:
  90. ret = config.getoption(name) # 'default' arg won't work as expected
  91. if ret is None:
  92. ret = config.getini(name)
  93. if ret:
  94. return ret
  95. def pytest_addoption(parser):
  96. """Add options to control log capturing."""
  97. group = parser.getgroup("logging")
  98. def add_option_ini(option, dest, default=None, type=None, **kwargs):
  99. parser.addini(
  100. dest, default=default, type=type, help="default value for " + option
  101. )
  102. group.addoption(option, dest=dest, **kwargs)
  103. add_option_ini(
  104. "--no-print-logs",
  105. dest="log_print",
  106. action="store_const",
  107. const=False,
  108. default=True,
  109. type="bool",
  110. help="disable printing caught logs on failed tests.",
  111. )
  112. add_option_ini(
  113. "--log-level",
  114. dest="log_level",
  115. default=None,
  116. help="logging level used by the logging module",
  117. )
  118. add_option_ini(
  119. "--log-format",
  120. dest="log_format",
  121. default=DEFAULT_LOG_FORMAT,
  122. help="log format as used by the logging module.",
  123. )
  124. add_option_ini(
  125. "--log-date-format",
  126. dest="log_date_format",
  127. default=DEFAULT_LOG_DATE_FORMAT,
  128. help="log date format as used by the logging module.",
  129. )
  130. parser.addini(
  131. "log_cli",
  132. default=False,
  133. type="bool",
  134. help='enable log display during test run (also known as "live logging").',
  135. )
  136. add_option_ini(
  137. "--log-cli-level", dest="log_cli_level", default=None, help="cli logging level."
  138. )
  139. add_option_ini(
  140. "--log-cli-format",
  141. dest="log_cli_format",
  142. default=None,
  143. help="log format as used by the logging module.",
  144. )
  145. add_option_ini(
  146. "--log-cli-date-format",
  147. dest="log_cli_date_format",
  148. default=None,
  149. help="log date format as used by the logging module.",
  150. )
  151. add_option_ini(
  152. "--log-file",
  153. dest="log_file",
  154. default=None,
  155. help="path to a file when logging will be written to.",
  156. )
  157. add_option_ini(
  158. "--log-file-level",
  159. dest="log_file_level",
  160. default=None,
  161. help="log file logging level.",
  162. )
  163. add_option_ini(
  164. "--log-file-format",
  165. dest="log_file_format",
  166. default=DEFAULT_LOG_FORMAT,
  167. help="log format as used by the logging module.",
  168. )
  169. add_option_ini(
  170. "--log-file-date-format",
  171. dest="log_file_date_format",
  172. default=DEFAULT_LOG_DATE_FORMAT,
  173. help="log date format as used by the logging module.",
  174. )
  175. @contextmanager
  176. def catching_logs(handler, formatter=None, level=None):
  177. """Context manager that prepares the whole logging machinery properly."""
  178. root_logger = logging.getLogger()
  179. if formatter is not None:
  180. handler.setFormatter(formatter)
  181. if level is not None:
  182. handler.setLevel(level)
  183. # Adding the same handler twice would confuse logging system.
  184. # Just don't do that.
  185. add_new_handler = handler not in root_logger.handlers
  186. if add_new_handler:
  187. root_logger.addHandler(handler)
  188. if level is not None:
  189. orig_level = root_logger.level
  190. root_logger.setLevel(min(orig_level, level))
  191. try:
  192. yield handler
  193. finally:
  194. if level is not None:
  195. root_logger.setLevel(orig_level)
  196. if add_new_handler:
  197. root_logger.removeHandler(handler)
  198. class LogCaptureHandler(logging.StreamHandler):
  199. """A logging handler that stores log records and the log text."""
  200. def __init__(self):
  201. """Creates a new log handler."""
  202. logging.StreamHandler.__init__(self, py.io.TextIO())
  203. self.records = []
  204. def emit(self, record):
  205. """Keep the log records in a list in addition to the log text."""
  206. self.records.append(record)
  207. logging.StreamHandler.emit(self, record)
  208. def reset(self):
  209. self.records = []
  210. self.stream = py.io.TextIO()
  211. class LogCaptureFixture(object):
  212. """Provides access and control of log capturing."""
  213. def __init__(self, item):
  214. """Creates a new funcarg."""
  215. self._item = item
  216. # dict of log name -> log level
  217. self._initial_log_levels = {} # Dict[str, int]
  218. def _finalize(self):
  219. """Finalizes the fixture.
  220. This restores the log levels changed by :meth:`set_level`.
  221. """
  222. # restore log levels
  223. for logger_name, level in self._initial_log_levels.items():
  224. logger = logging.getLogger(logger_name)
  225. logger.setLevel(level)
  226. @property
  227. def handler(self):
  228. """
  229. :rtype: LogCaptureHandler
  230. """
  231. return self._item.catch_log_handler
  232. def get_records(self, when):
  233. """
  234. Get the logging records for one of the possible test phases.
  235. :param str when:
  236. Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
  237. :rtype: List[logging.LogRecord]
  238. :return: the list of captured records at the given stage
  239. .. versionadded:: 3.4
  240. """
  241. handler = self._item.catch_log_handlers.get(when)
  242. if handler:
  243. return handler.records
  244. else:
  245. return []
  246. @property
  247. def text(self):
  248. """Returns the formatted log text."""
  249. return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
  250. @property
  251. def records(self):
  252. """Returns the list of log records."""
  253. return self.handler.records
  254. @property
  255. def record_tuples(self):
  256. """Returns a list of a stripped down version of log records intended
  257. for use in assertion comparison.
  258. The format of the tuple is:
  259. (logger_name, log_level, message)
  260. """
  261. return [(r.name, r.levelno, r.getMessage()) for r in self.records]
  262. @property
  263. def messages(self):
  264. """Returns a list of format-interpolated log messages.
  265. Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
  266. are all interpolated.
  267. Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
  268. levels, timestamps, etc, making exact comparisons more reliable.
  269. Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
  270. to the logging functions) is not included, as this is added by the formatter in the handler.
  271. .. versionadded:: 3.7
  272. """
  273. return [r.getMessage() for r in self.records]
  274. def clear(self):
  275. """Reset the list of log records and the captured log text."""
  276. self.handler.reset()
  277. def set_level(self, level, logger=None):
  278. """Sets the level for capturing of logs. The level will be restored to its previous value at the end of
  279. the test.
  280. :param int level: the logger to level.
  281. :param str logger: the logger to update the level. If not given, the root logger level is updated.
  282. .. versionchanged:: 3.4
  283. The levels of the loggers changed by this function will be restored to their initial values at the
  284. end of the test.
  285. """
  286. logger_name = logger
  287. logger = logging.getLogger(logger_name)
  288. # save the original log-level to restore it during teardown
  289. self._initial_log_levels.setdefault(logger_name, logger.level)
  290. logger.setLevel(level)
  291. @contextmanager
  292. def at_level(self, level, logger=None):
  293. """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
  294. level is restored to its original value.
  295. :param int level: the logger to level.
  296. :param str logger: the logger to update the level. If not given, the root logger level is updated.
  297. """
  298. logger = logging.getLogger(logger)
  299. orig_level = logger.level
  300. logger.setLevel(level)
  301. try:
  302. yield
  303. finally:
  304. logger.setLevel(orig_level)
  305. @pytest.fixture
  306. def caplog(request):
  307. """Access and control log capturing.
  308. Captured logs are available through the following properties/methods::
  309. * caplog.text -> string containing formatted log output
  310. * caplog.records -> list of logging.LogRecord instances
  311. * caplog.record_tuples -> list of (logger_name, level, message) tuples
  312. * caplog.clear() -> clear captured records and formatted log output string
  313. """
  314. result = LogCaptureFixture(request.node)
  315. yield result
  316. result._finalize()
  317. def get_actual_log_level(config, *setting_names):
  318. """Return the actual logging level."""
  319. for setting_name in setting_names:
  320. log_level = config.getoption(setting_name)
  321. if log_level is None:
  322. log_level = config.getini(setting_name)
  323. if log_level:
  324. break
  325. else:
  326. return
  327. if isinstance(log_level, six.string_types):
  328. log_level = log_level.upper()
  329. try:
  330. return int(getattr(logging, log_level, log_level))
  331. except ValueError:
  332. # Python logging does not recognise this as a logging level
  333. raise pytest.UsageError(
  334. "'{}' is not recognized as a logging level name for "
  335. "'{}'. Please consider passing the "
  336. "logging level num instead.".format(log_level, setting_name)
  337. )
  338. # run after terminalreporter/capturemanager are configured
  339. @pytest.hookimpl(trylast=True)
  340. def pytest_configure(config):
  341. config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
  342. class LoggingPlugin(object):
  343. """Attaches to the logging module and captures log messages for each test.
  344. """
  345. def __init__(self, config):
  346. """Creates a new plugin to capture log messages.
  347. The formatter can be safely shared across all handlers so
  348. create a single one for the entire test session here.
  349. """
  350. self._config = config
  351. self.print_logs = get_option_ini(config, "log_print")
  352. self.formatter = self._create_formatter(
  353. get_option_ini(config, "log_format"),
  354. get_option_ini(config, "log_date_format"),
  355. )
  356. self.log_level = get_actual_log_level(config, "log_level")
  357. self.log_file_level = get_actual_log_level(config, "log_file_level")
  358. self.log_file_format = get_option_ini(config, "log_file_format", "log_format")
  359. self.log_file_date_format = get_option_ini(
  360. config, "log_file_date_format", "log_date_format"
  361. )
  362. self.log_file_formatter = logging.Formatter(
  363. self.log_file_format, datefmt=self.log_file_date_format
  364. )
  365. log_file = get_option_ini(config, "log_file")
  366. if log_file:
  367. self.log_file_handler = logging.FileHandler(
  368. log_file, mode="w", encoding="UTF-8"
  369. )
  370. self.log_file_handler.setFormatter(self.log_file_formatter)
  371. else:
  372. self.log_file_handler = None
  373. self.log_cli_handler = None
  374. self.live_logs_context = lambda: dummy_context_manager()
  375. # Note that the lambda for the live_logs_context is needed because
  376. # live_logs_context can otherwise not be entered multiple times due
  377. # to limitations of contextlib.contextmanager.
  378. if self._log_cli_enabled():
  379. self._setup_cli_logging()
  380. def _create_formatter(self, log_format, log_date_format):
  381. # color option doesn't exist if terminal plugin is disabled
  382. color = getattr(self._config.option, "color", "no")
  383. if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
  384. log_format
  385. ):
  386. formatter = ColoredLevelFormatter(
  387. create_terminal_writer(self._config), log_format, log_date_format
  388. )
  389. else:
  390. formatter = logging.Formatter(log_format, log_date_format)
  391. if not six.PY2:
  392. formatter._style = PercentStyleMultiline(formatter._style._fmt)
  393. return formatter
  394. def _setup_cli_logging(self):
  395. config = self._config
  396. terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
  397. if terminal_reporter is None:
  398. # terminal reporter is disabled e.g. by pytest-xdist.
  399. return
  400. capture_manager = config.pluginmanager.get_plugin("capturemanager")
  401. # if capturemanager plugin is disabled, live logging still works.
  402. log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
  403. log_cli_formatter = self._create_formatter(
  404. get_option_ini(config, "log_cli_format", "log_format"),
  405. get_option_ini(config, "log_cli_date_format", "log_date_format"),
  406. )
  407. log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
  408. self.log_cli_handler = log_cli_handler
  409. self.live_logs_context = lambda: catching_logs(
  410. log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
  411. )
  412. def set_log_path(self, fname):
  413. """Public method, which can set filename parameter for
  414. Logging.FileHandler(). Also creates parent directory if
  415. it does not exist.
  416. .. warning::
  417. Please considered as an experimental API.
  418. """
  419. fname = Path(fname)
  420. if not fname.is_absolute():
  421. fname = Path(self._config.rootdir, fname)
  422. if not fname.parent.exists():
  423. fname.parent.mkdir(exist_ok=True, parents=True)
  424. self.log_file_handler = logging.FileHandler(
  425. str(fname), mode="w", encoding="UTF-8"
  426. )
  427. self.log_file_handler.setFormatter(self.log_file_formatter)
  428. def _log_cli_enabled(self):
  429. """Return True if log_cli should be considered enabled, either explicitly
  430. or because --log-cli-level was given in the command-line.
  431. """
  432. return self._config.getoption(
  433. "--log-cli-level"
  434. ) is not None or self._config.getini("log_cli")
  435. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  436. def pytest_collection(self):
  437. with self.live_logs_context():
  438. if self.log_cli_handler:
  439. self.log_cli_handler.set_when("collection")
  440. if self.log_file_handler is not None:
  441. with catching_logs(self.log_file_handler, level=self.log_file_level):
  442. yield
  443. else:
  444. yield
  445. @contextmanager
  446. def _runtest_for(self, item, when):
  447. with self._runtest_for_main(item, when):
  448. if self.log_file_handler is not None:
  449. with catching_logs(self.log_file_handler, level=self.log_file_level):
  450. yield
  451. else:
  452. yield
  453. @contextmanager
  454. def _runtest_for_main(self, item, when):
  455. """Implements the internals of pytest_runtest_xxx() hook."""
  456. with catching_logs(
  457. LogCaptureHandler(), formatter=self.formatter, level=self.log_level
  458. ) as log_handler:
  459. if self.log_cli_handler:
  460. self.log_cli_handler.set_when(when)
  461. if item is None:
  462. yield # run the test
  463. return
  464. if not hasattr(item, "catch_log_handlers"):
  465. item.catch_log_handlers = {}
  466. item.catch_log_handlers[when] = log_handler
  467. item.catch_log_handler = log_handler
  468. try:
  469. yield # run test
  470. finally:
  471. if when == "teardown":
  472. del item.catch_log_handler
  473. del item.catch_log_handlers
  474. if self.print_logs:
  475. # Add a captured log section to the report.
  476. log = log_handler.stream.getvalue().strip()
  477. item.add_report_section(when, "log", log)
  478. @pytest.hookimpl(hookwrapper=True)
  479. def pytest_runtest_setup(self, item):
  480. with self._runtest_for(item, "setup"):
  481. yield
  482. @pytest.hookimpl(hookwrapper=True)
  483. def pytest_runtest_call(self, item):
  484. with self._runtest_for(item, "call"):
  485. yield
  486. @pytest.hookimpl(hookwrapper=True)
  487. def pytest_runtest_teardown(self, item):
  488. with self._runtest_for(item, "teardown"):
  489. yield
  490. @pytest.hookimpl(hookwrapper=True)
  491. def pytest_runtest_logstart(self):
  492. if self.log_cli_handler:
  493. self.log_cli_handler.reset()
  494. with self._runtest_for(None, "start"):
  495. yield
  496. @pytest.hookimpl(hookwrapper=True)
  497. def pytest_runtest_logfinish(self):
  498. with self._runtest_for(None, "finish"):
  499. yield
  500. @pytest.hookimpl(hookwrapper=True)
  501. def pytest_runtest_logreport(self):
  502. with self._runtest_for(None, "logreport"):
  503. yield
  504. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  505. def pytest_sessionfinish(self):
  506. with self.live_logs_context():
  507. if self.log_cli_handler:
  508. self.log_cli_handler.set_when("sessionfinish")
  509. if self.log_file_handler is not None:
  510. try:
  511. with catching_logs(
  512. self.log_file_handler, level=self.log_file_level
  513. ):
  514. yield
  515. finally:
  516. # Close the FileHandler explicitly.
  517. # (logging.shutdown might have lost the weakref?!)
  518. self.log_file_handler.close()
  519. else:
  520. yield
  521. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  522. def pytest_sessionstart(self):
  523. with self.live_logs_context():
  524. if self.log_cli_handler:
  525. self.log_cli_handler.set_when("sessionstart")
  526. if self.log_file_handler is not None:
  527. with catching_logs(self.log_file_handler, level=self.log_file_level):
  528. yield
  529. else:
  530. yield
  531. @pytest.hookimpl(hookwrapper=True)
  532. def pytest_runtestloop(self, session):
  533. """Runs all collected test items."""
  534. if session.config.option.collectonly:
  535. yield
  536. return
  537. if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
  538. # setting verbose flag is needed to avoid messy test progress output
  539. self._config.option.verbose = 1
  540. with self.live_logs_context():
  541. if self.log_file_handler is not None:
  542. with catching_logs(self.log_file_handler, level=self.log_file_level):
  543. yield # run all the tests
  544. else:
  545. yield # run all the tests
  546. class _LiveLoggingStreamHandler(logging.StreamHandler):
  547. """
  548. Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
  549. in each test.
  550. During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
  551. and won't appear in the terminal.
  552. """
  553. def __init__(self, terminal_reporter, capture_manager):
  554. """
  555. :param _pytest.terminal.TerminalReporter terminal_reporter:
  556. :param _pytest.capture.CaptureManager capture_manager:
  557. """
  558. logging.StreamHandler.__init__(self, stream=terminal_reporter)
  559. self.capture_manager = capture_manager
  560. self.reset()
  561. self.set_when(None)
  562. self._test_outcome_written = False
  563. def reset(self):
  564. """Reset the handler; should be called before the start of each test"""
  565. self._first_record_emitted = False
  566. def set_when(self, when):
  567. """Prepares for the given test phase (setup/call/teardown)"""
  568. self._when = when
  569. self._section_name_shown = False
  570. if when == "start":
  571. self._test_outcome_written = False
  572. def emit(self, record):
  573. ctx_manager = (
  574. self.capture_manager.global_and_fixture_disabled()
  575. if self.capture_manager
  576. else dummy_context_manager()
  577. )
  578. with ctx_manager:
  579. if not self._first_record_emitted:
  580. self.stream.write("\n")
  581. self._first_record_emitted = True
  582. elif self._when in ("teardown", "finish"):
  583. if not self._test_outcome_written:
  584. self._test_outcome_written = True
  585. self.stream.write("\n")
  586. if not self._section_name_shown and self._when:
  587. self.stream.section("live log " + self._when, sep="-", bold=True)
  588. self._section_name_shown = True
  589. logging.StreamHandler.emit(self, record)