Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/logs.py : 36%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# cardinal_pythonlib/logs.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
25**Support functions for logging.**
27See https://docs.python.org/3.4/howto/logging.html#library-config
29USER CODE should use the following general methods.
31(a) Simple:
33 .. code-block:: python
35 import logging
36 log = logging.getLogger(__name__) # for your own logs
37 logging.basicConfig()
39(b) More complex:
41 .. code-block:: python
43 import logging
44 log = logging.getLogger(__name__)
45 logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATEFMT,
46 level=loglevel)
48(c) Using colour conveniently:
50 .. code-block:: python
52 import logging
53 mylogger = logging.getLogger(__name__)
54 rootlogger = logging.getLogger()
56 from whisker.log import configure_logger_for_colour
57 configure_logger_for_colour(rootlogger)
60LIBRARY CODE should use the following general methods.
62.. code-block:: python
64 import logging
65 log = logging.getLogger(__name__)
67 # ... and if you want to suppress output unless the user configures logs:
68 log.addHandler(logging.NullHandler())
69 # ... which only needs to be done in the __init__.py for the package
70 # https://stackoverflow.com/questions/12296214
72 # LIBRARY CODE SHOULD NOT ADD ANY OTHER HANDLERS; see above.
74DO NOT call this module "logging"! Many things may get confused.
76"""
78from html import escape
79from inspect import Parameter, signature
80import json
81import logging
82import os
83from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Union
85from colorlog import ColoredFormatter
87# =============================================================================
88# Quick configuration of a specific log format
89# =============================================================================
91LOG_FORMAT = '%(asctime)s.%(msecs)03d:%(levelname)s:%(name)s:%(message)s'
92LOG_FORMAT_WITH_PID = (
93 f'%(asctime)s.%(msecs)03d:%(levelname)s:{os.getpid()}:%(name)s:%(message)s'
94)
96LOG_DATEFMT = '%Y-%m-%d %H:%M:%S'
98LOG_COLORS = {'DEBUG': 'cyan',
99 'INFO': 'green',
100 'WARNING': 'bold_yellow',
101 'ERROR': 'bold_red',
102 'CRITICAL': 'bold_white,bg_red'}
105def get_monochrome_handler(
106 extranames: List[str] = None,
107 with_process_id: bool = False,
108 with_thread_id: bool = False,
109 stream: TextIO = None) -> logging.StreamHandler:
110 """
111 Gets a monochrome log handler using a standard format.
113 Args:
114 extranames: additional names to append to the logger's name
115 with_process_id: include the process ID in the logger's name?
116 with_thread_id: include the thread ID in the logger's name?
117 stream: ``TextIO`` stream to send log output to
119 Returns:
120 the :class:`logging.StreamHandler`
122 """
123 fmt = "%(asctime)s.%(msecs)03d"
124 if with_process_id or with_thread_id:
125 procinfo = [] # type: List[str]
126 if with_process_id:
127 procinfo.append("p%(process)d")
128 if with_thread_id:
129 procinfo.append("t%(thread)d")
130 fmt += f" [{'.'.join(procinfo)}]"
131 extras = ":" + ":".join(extranames) if extranames else ""
132 fmt += f" %(name)s{extras}:%(levelname)s: "
133 fmt += "%(message)s"
134 f = logging.Formatter(fmt, datefmt=LOG_DATEFMT, style='%')
135 h = logging.StreamHandler(stream)
136 h.setFormatter(f)
137 return h
140def get_colour_handler(extranames: List[str] = None,
141 with_process_id: bool = False,
142 with_thread_id: bool = False,
143 stream: TextIO = None) -> logging.StreamHandler:
144 """
145 Gets a colour log handler using a standard format.
147 Args:
148 extranames: additional names to append to the logger's name
149 with_process_id: include the process ID in the logger's name?
150 with_thread_id: include the thread ID in the logger's name?
151 stream: ``TextIO`` stream to send log output to
153 Returns:
154 the :class:`logging.StreamHandler`
156 """
157 fmt = "%(white)s%(asctime)s.%(msecs)03d" # this is dim white = grey
158 if with_process_id or with_thread_id:
159 procinfo = [] # type: List[str]
160 if with_process_id:
161 procinfo.append("p%(process)d")
162 if with_thread_id:
163 procinfo.append("t%(thread)d")
164 fmt += f" [{'.'.join(procinfo)}]"
165 extras = ":" + ":".join(extranames) if extranames else ""
166 fmt += f" %(name)s{extras}:%(levelname)s: "
167 fmt += "%(reset)s%(log_color)s%(message)s"
168 cf = ColoredFormatter(fmt,
169 datefmt=LOG_DATEFMT,
170 reset=True,
171 log_colors=LOG_COLORS,
172 secondary_log_colors={},
173 style='%')
174 ch = logging.StreamHandler(stream)
175 ch.setFormatter(cf)
176 return ch
179def configure_logger_for_colour(logger: logging.Logger,
180 level: int = logging.INFO,
181 remove_existing: bool = False,
182 extranames: List[str] = None,
183 with_process_id: bool = False,
184 with_thread_id: bool = False) -> None:
185 """
186 Applies a preconfigured datetime/colour scheme to a logger.
188 Should ONLY be called from the ``if __name__ == 'main'`` script;
189 see https://docs.python.org/3.4/howto/logging.html#library-config.
191 Args:
192 logger: logger to modify
193 level: log level to set
194 remove_existing: remove existing handlers from logger first?
195 extranames: additional names to append to the logger's name
196 with_process_id: include the process ID in the logger's name?
197 with_thread_id: include the thread ID in the logger's name?
198 """
199 if remove_existing:
200 logger.handlers = [] # https://stackoverflow.com/questions/7484454
201 handler = get_colour_handler(extranames,
202 with_process_id=with_process_id,
203 with_thread_id=with_thread_id)
204 handler.setLevel(level)
205 logger.addHandler(handler)
206 logger.setLevel(level)
209def main_only_quicksetup_rootlogger(level: int = logging.DEBUG,
210 with_process_id: bool = False,
211 with_thread_id: bool = False) -> None:
212 """
213 Quick function to set up the root logger for colour.
215 Should ONLY be called from the ``if __name__ == 'main'`` script;
216 see https://docs.python.org/3.4/howto/logging.html#library-config.
218 Args:
219 level: log level to set
220 with_process_id: include the process ID in the logger's name?
221 with_thread_id: include the thread ID in the logger's name?
222 """
223 # Nasty. Only call from "if __name__ == '__main__'" clauses!
224 rootlogger = logging.getLogger()
225 configure_logger_for_colour(rootlogger, level, remove_existing=True,
226 with_process_id=with_process_id,
227 with_thread_id=with_thread_id)
228 # logging.basicConfig(level=level)
231# =============================================================================
232# Generic log functions
233# =============================================================================
235def remove_all_logger_handlers(logger: logging.Logger) -> None:
236 """
237 Remove all handlers from a logger.
239 Args:
240 logger: logger to modify
241 """
242 while logger.handlers:
243 h = logger.handlers[0]
244 logger.removeHandler(h)
247def reset_logformat(logger: logging.Logger,
248 fmt: str,
249 datefmt: str = '%Y-%m-%d %H:%M:%S') -> None:
250 """
251 Create a new formatter and apply it to the logger.
253 :func:`logging.basicConfig` won't reset the formatter if another module
254 has called it, so always set the formatter like this.
256 Args:
257 logger: logger to modify
258 fmt: passed to the ``fmt=`` argument of :class:`logging.Formatter`
259 datefmt: passed to the ``datefmt=`` argument of
260 :class:`logging.Formatter`
261 """
262 handler = logging.StreamHandler()
263 formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
264 handler.setFormatter(formatter)
265 remove_all_logger_handlers(logger)
266 logger.addHandler(handler)
267 logger.propagate = False
270def reset_logformat_timestamped(logger: logging.Logger,
271 extraname: str = "",
272 level: int = logging.INFO) -> None:
273 """
274 Apply a simple time-stamped log format to an existing logger, and set
275 its loglevel to either ``logging.DEBUG`` or ``logging.INFO``.
277 Args:
278 logger: logger to modify
279 extraname: additional name to append to the logger's name
280 level: log level to set
281 """
282 namebit = extraname + ":" if extraname else ""
283 fmt = ("%(asctime)s.%(msecs)03d:%(levelname)s:%(name)s:" + namebit +
284 "%(message)s")
285 # logger.info(fmt)
286 reset_logformat(logger, fmt=fmt)
287 # logger.info(fmt)
288 logger.setLevel(level)
291# =============================================================================
292# Helper functions
293# =============================================================================
295def configure_all_loggers_for_colour(remove_existing: bool = True) -> None:
296 """
297 Applies a preconfigured datetime/colour scheme to ALL logger.
299 Should ONLY be called from the ``if __name__ == 'main'`` script;
300 see https://docs.python.org/3.4/howto/logging.html#library-config.
302 Generally MORE SENSIBLE just to apply a handler to the root logger.
304 Args:
305 remove_existing: remove existing handlers from logger first?
307 """
308 handler = get_colour_handler()
309 apply_handler_to_all_logs(handler, remove_existing=remove_existing)
312def apply_handler_to_root_log(handler: logging.Handler,
313 remove_existing: bool = False) -> None:
314 """
315 Applies a handler to all logs, optionally removing existing handlers.
317 Should ONLY be called from the ``if __name__ == 'main'`` script;
318 see https://docs.python.org/3.4/howto/logging.html#library-config.
320 Generally MORE SENSIBLE just to apply a handler to the root logger.
322 Args:
323 handler: the handler to apply
324 remove_existing: remove existing handlers from logger first?
325 """
326 rootlog = logging.getLogger()
327 if remove_existing:
328 rootlog.handlers = []
329 rootlog.addHandler(handler)
332def apply_handler_to_all_logs(handler: logging.Handler,
333 remove_existing: bool = False) -> None:
334 """
335 Applies a handler to all logs, optionally removing existing handlers.
337 Should ONLY be called from the ``if __name__ == 'main'`` script;
338 see https://docs.python.org/3.4/howto/logging.html#library-config.
340 Generally MORE SENSIBLE just to apply a handler to the root logger.
342 Args:
343 handler: the handler to apply
344 remove_existing: remove existing handlers from logger first?
345 """
346 # noinspection PyUnresolvedReferences
347 for name, obj in logging.Logger.manager.loggerDict.items():
348 if remove_existing:
349 obj.handlers = [] # https://stackoverflow.com/questions/7484454
350 obj.addHandler(handler)
353def copy_root_log_to_file(filename: str,
354 fmt: str = LOG_FORMAT,
355 datefmt: str = LOG_DATEFMT) -> None:
356 """
357 Copy all currently configured logs to the specified file.
359 Should ONLY be called from the ``if __name__ == 'main'`` script;
360 see https://docs.python.org/3.4/howto/logging.html#library-config.
361 """
362 fh = logging.FileHandler(filename)
363 # default file mode is 'a' for append
364 formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
365 fh.setFormatter(formatter)
366 apply_handler_to_root_log(fh)
369def copy_all_logs_to_file(filename: str,
370 fmt: str = LOG_FORMAT,
371 datefmt: str = LOG_DATEFMT) -> None:
372 """
373 Copy all currently configured logs to the specified file.
375 Should ONLY be called from the ``if __name__ == 'main'`` script;
376 see https://docs.python.org/3.4/howto/logging.html#library-config.
378 Args:
379 filename: file to send log output to
380 fmt: passed to the ``fmt=`` argument of :class:`logging.Formatter`
381 datefmt: passed to the ``datefmt=`` argument of
382 :class:`logging.Formatter`
383 """
384 fh = logging.FileHandler(filename)
385 # default file mode is 'a' for append
386 formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
387 fh.setFormatter(formatter)
388 apply_handler_to_all_logs(fh)
391# noinspection PyProtectedMember
392def get_formatter_report(f: logging.Formatter) -> Optional[Dict[str, str]]:
393 """
394 Returns information on a log formatter, as a dictionary.
395 For debugging.
396 """
397 if f is None:
398 return None
399 return {
400 '_fmt': f._fmt,
401 'datefmt': f.datefmt,
402 '_style': str(f._style),
403 }
406def get_handler_report(h: logging.Handler) -> Dict[str, Any]:
407 """
408 Returns information on a log handler, as a dictionary.
409 For debugging.
410 """
411 # noinspection PyUnresolvedReferences
412 return {
413 'get_name()': h.get_name(),
414 'level': h.level,
415 'formatter': get_formatter_report(h.formatter),
416 'filters': h.filters,
417 }
420def get_log_report(log: Union[logging.Logger,
421 logging.PlaceHolder]) -> Dict[str, Any]:
422 """
423 Returns information on a log, as a dictionary. For debugging.
424 """
425 if isinstance(log, logging.Logger):
426 # suppress invalid error for Logger.manager:
427 # noinspection PyUnresolvedReferences
428 return {
429 '(object)': str(log),
430 'level': log.level,
431 'disabled': log.disabled,
432 'propagate': log.propagate,
433 'parent': str(log.parent),
434 'manager': str(log.manager),
435 'handlers': [get_handler_report(h) for h in log.handlers],
436 }
437 elif isinstance(log, logging.PlaceHolder):
438 return {
439 "(object)": str(log),
440 }
441 else:
442 raise ValueError(f"Unknown object type: {log!r}")
445def print_report_on_all_logs() -> None:
446 """
447 Use :func:`print` to report information on all logs.
448 """
449 d = {}
450 # noinspection PyUnresolvedReferences
451 for name, obj in logging.Logger.manager.loggerDict.items():
452 d[name] = get_log_report(obj)
453 rootlogger = logging.getLogger()
454 d['(root logger)'] = get_log_report(rootlogger)
455 print(json.dumps(d, sort_keys=True, indent=4, separators=(',', ': ')))
458def set_level_for_logger_and_its_handlers(log: logging.Logger,
459 level: int) -> None:
460 """
461 Set a log level for a log and all its handlers.
463 Args:
464 log: log to modify
465 level: log level to set
466 """
467 log.setLevel(level)
468 for h in log.handlers: # type: logging.Handler
469 h.setLevel(level)
472# =============================================================================
473# HTML formatter
474# =============================================================================
476class HtmlColorFormatter(logging.Formatter):
477 """
478 Class to format Python logs in coloured HTML.
479 """
480 log_colors = {
481 logging.DEBUG: '#008B8B', # dark cyan
482 logging.INFO: '#00FF00', # green
483 logging.WARNING: '#FFFF00', # yellow
484 logging.ERROR: '#FF0000', # red
485 logging.CRITICAL: '#FF0000', # red
486 }
487 log_background_colors = {
488 logging.DEBUG: None,
489 logging.INFO: None,
490 logging.WARNING: None,
491 logging.ERROR: None,
492 logging.CRITICAL: '#FFFFFF', # white
493 }
495 def __init__(self, append_br: bool = False,
496 replace_nl_with_br: bool = True) -> None:
497 r"""
498 Args:
499 append_br: append ``<br>`` to each line?
500 replace_nl_with_br: replace ``\n`` with ``<br>`` in messages?
502 See https://hg.python.org/cpython/file/3.5/Lib/logging/__init__.py
503 """
504 super().__init__(
505 fmt='%(message)s',
506 datefmt='%Y-%m-%d %H:%M:%S',
507 style='%'
508 )
509 self.append_br = append_br
510 self.replace_nl_with_br = replace_nl_with_br
512 def format(self, record: logging.LogRecord) -> str:
513 """
514 Internal function to format the :class:`LogRecord` as HTML.
516 See https://docs.python.org/3.4/library/logging.html#logging.LogRecord
517 """
519 # message = super().format(record)
520 super().format(record)
521 # Since fmt does not contain asctime, the Formatter.format()
522 # will not write asctime (since its usesTime()) function will be
523 # false. Therefore:
524 record.asctime = self.formatTime(record, self.datefmt)
525 bg_col = self.log_background_colors[record.levelno]
526 msg = escape(record.getMessage())
527 # escape() won't replace \n but will replace & etc.
528 if self.replace_nl_with_br:
529 msg = msg.replace("\n", "<br>")
530 html = (
531 '<span style="color:#008B8B">{time}.{ms:03d} {name}:{lvname}: '
532 '</span><span style="color:{color}{bg}">{msg}</font>{br}'.format(
533 time=record.asctime,
534 ms=int(record.msecs),
535 name=record.name,
536 lvname=record.levelname,
537 color=self.log_colors[record.levelno],
538 msg=msg,
539 bg=f";background-color:{bg_col}" if bg_col else "",
540 br="<br>" if self.append_br else "",
541 )
542 )
543 # print("record.__dict__: {}".format(record.__dict__))
544 # print("html: {}".format(html))
545 return html
548# =============================================================================
549# HtmlColorHandler
550# =============================================================================
552class HtmlColorHandler(logging.StreamHandler):
553 """
554 HTML handler (using :class:`HtmlColorFormatter`) that sends output to a
555 function, e.g. for display in a Qt window
556 """
557 def __init__(self, logfunction: Callable[[str], None],
558 level: int = logging.INFO) -> None:
559 super().__init__()
560 self.logfunction = logfunction
561 self.setFormatter(HtmlColorFormatter())
562 self.setLevel(level)
564 def emit(self, record: logging.LogRecord) -> None:
565 """
566 Internal function to process a :class:`LogRecord`.
567 """
568 # noinspection PyBroadException
569 try:
570 html = self.format(record)
571 self.logfunction(html)
572 except: # nopep8
573 self.handleError(record)
576# =============================================================================
577# Brace formatters, for log.info("{}, {}", "hello", "world")
578# =============================================================================
580# - https://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles # noqa
581# - https://stackoverflow.com/questions/13131400/logging-variable-data-with-new-format-string # noqa
582# - https://stackoverflow.com/questions/13131400/logging-variable-data-with-new-format-string/24683360#24683360 # noqa
583# ... plus modifications to use inspect.signature() not inspect.getargspec()
584# ... plus a performance tweak so we're not calling signature() every time
585# See also:
586# - https://www.simonmweber.com/2014/11/24/python-logging-traps.html
588class BraceMessage(object):
589 """
590 Class to represent a message that includes a message including braces
591 (``{}``) and a set of ``args``/``kwargs``. When converted to a ``str``,
592 the message is realized via ``msg.format(*args, **kwargs)``.
593 """
594 def __init__(self,
595 fmt: str,
596 args: Tuple[Any, ...],
597 kwargs: Dict[str, Any]) -> None:
598 # This version uses args and kwargs, not *args and **kwargs, for
599 # performance reasons:
600 # https://stackoverflow.com/questions/31992424/performance-implications-of-unpacking-dictionaries-in-python # noqa
601 # ... and since we control creation entirely, we may as well go fast
602 self.fmt = fmt
603 self.args = args
604 self.kwargs = kwargs
605 # print("Creating BraceMessage with: fmt={}, args={}, "
606 # "kwargs={}".format(repr(fmt), repr(args), repr(kwargs)))
608 def __str__(self) -> str:
609 return self.fmt.format(*self.args, **self.kwargs)
612class BraceStyleAdapter(logging.LoggerAdapter):
613 def __init__(self,
614 logger: logging.Logger,
615 pass_special_logger_args: bool = True,
616 strip_special_logger_args_from_fmt: bool = False) -> None:
617 """
618 Wraps a logger so we can use ``{}``-style string formatting.
620 Args:
621 logger:
622 a logger
623 pass_special_logger_args:
624 should we continue to pass any special arguments to the logger
625 itself? True is standard; False probably brings a slight
626 performance benefit, but prevents log.exception() from working
627 properly, as the 'exc_info' parameter will be stripped.
628 strip_special_logger_args_from_fmt:
629 If we're passing special arguments to the logger, should we
630 remove them from the argments passed to the string formatter?
631 There is no obvious cost to saying no.
633 Specimen use:
635 .. code-block:: python
637 import logging
638 from cardinal_pythonlib.logs import BraceStyleAdapter, main_only_quicksetup_rootlogger
640 log = BraceStyleAdapter(logging.getLogger(__name__))
642 main_only_quicksetup_rootlogger(level=logging.DEBUG)
644 log.info("Hello {}, {title} {surname}!", "world", title="Mr", surname="Smith")
645 # 2018-09-17 16:13:50.404 __main__:INFO: Hello world, Mr Smith!
647 """ # noqa
648 # noinspection PyTypeChecker
649 super().__init__(logger=logger, extra=None)
650 self.pass_special_logger_args = pass_special_logger_args
651 self.strip_special_logger_args_from_fmt = strip_special_logger_args_from_fmt # noqa
652 # getargspec() returns:
653 # named tuple: ArgSpec(args, varargs, keywords, defaults)
654 # ... args = list of parameter names
655 # ... varargs = names of the * parameters, or None
656 # ... keywords = names of the ** parameters, or None
657 # ... defaults = tuple of default argument values, or None
658 # signature() returns a Signature object:
659 # ... parameters: ordered mapping of name -> Parameter
660 # ... ... https://docs.python.org/3/library/inspect.html#inspect.Parameter # noqa
661 # Direct equivalence:
662 # https://github.com/praw-dev/praw/issues/541
663 # So, old:
664 # logargnames = getargspec(self.logger._log).args[1:]
665 # and new:
666 # noinspection PyProtectedMember
667 sig = signature(self.logger._log)
668 self.logargnames = [p.name for p in sig.parameters.values()
669 if p.kind == Parameter.POSITIONAL_OR_KEYWORD]
670 # e.g.: ['level', 'msg', 'args', 'exc_info', 'extra', 'stack_info']
671 # print("self.logargnames: " + repr(self.logargnames))
673 def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None:
674 if self.isEnabledFor(level):
675 # print("log: msg={}, args={}, kwargs={}".format(
676 # repr(msg), repr(args), repr(kwargs)))
677 if self.pass_special_logger_args:
678 msg, log_kwargs = self.process(msg, kwargs)
679 # print("... log: msg={}, log_kwargs={}".format(
680 # repr(msg), repr(log_kwargs)))
681 else:
682 log_kwargs = {}
683 # noinspection PyProtectedMember
684 self.logger._log(level, BraceMessage(msg, args, kwargs), (),
685 **log_kwargs)
687 def process(self, msg: str,
688 kwargs: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
689 special_param_names = [k for k in kwargs.keys()
690 if k in self.logargnames]
691 log_kwargs = {k: kwargs[k] for k in special_param_names}
692 # ... also: remove them from the starting kwargs?
693 if self.strip_special_logger_args_from_fmt:
694 for k in special_param_names:
695 kwargs.pop(k)
696 return msg, log_kwargs
699def get_log_with_null_handler(name: str) -> logging.Logger:
700 """
701 For use by library functions. Returns a log with the specifed name that
702 has a null handler attached, and a :class:`BraceStyleAdapter`.
703 """
704 log = logging.getLogger(name)
705 log.addHandler(logging.NullHandler())
706 return log
709def get_brace_style_log_with_null_handler(name: str) -> BraceStyleAdapter:
710 """
711 For use by library functions. Returns a log with the specifed name that
712 has a null handler attached, and a :class:`BraceStyleAdapter`.
713 """
714 log = logging.getLogger(name)
715 log.addHandler(logging.NullHandler())
716 return BraceStyleAdapter(log)
719# =============================================================================
720# Testing
721# =============================================================================
723if __name__ == '__main__':
724 """
725 Command-line validation checks.
726 """
727 main_only_quicksetup_rootlogger(logging.INFO)
728 _log = BraceStyleAdapter(logging.getLogger(__name__))
729 _log.info("1. Hello!")
730 _log.info("1. Hello, {}!", "world")
731 _log.info("1. Hello, foo={foo}, bar={bar}!", foo="foo", bar="bar")
732 _log.info("1. Hello, {}; foo={foo}, bar={bar}!", "world", foo="foo",
733 bar="bar")
734 _log.info("1. Hello, {}; foo={foo}, bar={bar}!", "world", foo="foo",
735 bar="bar", extra={'somekey': 'somevalue'})