comparison env/lib/python3.9/site-packages/coloredlogs/__init__.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # Colored terminal output for Python's logging module.
2 #
3 # Author: Peter Odding <peter@peterodding.com>
4 # Last Change: December 10, 2020
5 # URL: https://coloredlogs.readthedocs.io
6
7 """
8 Colored terminal output for Python's :mod:`logging` module.
9
10 .. contents::
11 :local:
12
13 Getting started
14 ===============
15
16 The easiest way to get started is by importing :mod:`coloredlogs` and calling
17 :mod:`coloredlogs.install()` (similar to :func:`logging.basicConfig()`):
18
19 >>> import coloredlogs, logging
20 >>> coloredlogs.install(level='DEBUG')
21 >>> logger = logging.getLogger('some.module.name')
22 >>> logger.info("this is an informational message")
23 2015-10-22 19:13:52 peter-macbook some.module.name[28036] INFO this is an informational message
24
25 The :mod:`~coloredlogs.install()` function creates a :class:`ColoredFormatter`
26 that injects `ANSI escape sequences`_ into the log output.
27
28 .. _ANSI escape sequences: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
29
30 Environment variables
31 =====================
32
33 The following environment variables can be used to configure the
34 :mod:`coloredlogs` module without writing any code:
35
36 ============================= ============================ ==================================
37 Environment variable Default value Type of value
38 ============================= ============================ ==================================
39 ``$COLOREDLOGS_AUTO_INSTALL`` 'false' a boolean that controls whether
40 :func:`auto_install()` is called
41 ``$COLOREDLOGS_LOG_LEVEL`` 'INFO' a log level name
42 ``$COLOREDLOGS_LOG_FORMAT`` :data:`DEFAULT_LOG_FORMAT` a log format string
43 ``$COLOREDLOGS_DATE_FORMAT`` :data:`DEFAULT_DATE_FORMAT` a date/time format string
44 ``$COLOREDLOGS_LEVEL_STYLES`` :data:`DEFAULT_LEVEL_STYLES` see :func:`parse_encoded_styles()`
45 ``$COLOREDLOGS_FIELD_STYLES`` :data:`DEFAULT_FIELD_STYLES` see :func:`parse_encoded_styles()`
46 ============================= ============================ ==================================
47
48 If the environment variable `$NO_COLOR`_ is set (the value doesn't matter, even
49 an empty string will do) then :func:`coloredlogs.install()` will take this as a
50 hint that colors should not be used (unless the ``isatty=True`` override was
51 passed by the caller).
52
53 .. _$NO_COLOR: https://no-color.org/
54
55 Examples of customization
56 =========================
57
58 Here we'll take a look at some examples of how you can customize
59 :mod:`coloredlogs` using environment variables.
60
61 .. contents::
62 :local:
63
64 About the defaults
65 ------------------
66
67 Here's a screen shot of the default configuration for easy comparison with the
68 screen shots of the following customizations (this is the same screen shot that
69 is shown in the introduction):
70
71 .. image:: images/defaults.png
72 :alt: Screen shot of colored logging with defaults.
73
74 The screen shot above was taken from ``urxvt`` which doesn't support faint text
75 colors, otherwise the color of green used for `debug` messages would have
76 differed slightly from the color of green used for `spam` messages.
77
78 Apart from the `faint` style of the `spam` level, the default configuration of
79 `coloredlogs` sticks to the eight color palette defined by the original ANSI
80 standard, in order to provide a somewhat consistent experience across terminals
81 and terminal emulators.
82
83 Available text styles and colors
84 --------------------------------
85
86 Of course you are free to customize the default configuration, in this case you
87 can use any text style or color that you know is supported by your terminal.
88 You can use the ``humanfriendly --demo`` command to try out the supported text
89 styles and colors:
90
91 .. image:: http://humanfriendly.readthedocs.io/en/latest/_images/ansi-demo.png
92 :alt: Screen shot of the 'humanfriendly --demo' command.
93
94 Changing the log format
95 -----------------------
96
97 The simplest customization is to change the log format, for example:
98
99 .. literalinclude:: examples/custom-log-format.txt
100 :language: console
101
102 Here's what that looks like in a terminal (I always work in terminals with a
103 black background and white text):
104
105 .. image:: images/custom-log-format.png
106 :alt: Screen shot of colored logging with custom log format.
107
108 Changing the date/time format
109 -----------------------------
110
111 You can also change the date/time format, for example you can remove the date
112 part and leave only the time:
113
114 .. literalinclude:: examples/custom-datetime-format.txt
115 :language: console
116
117 Here's what it looks like in a terminal:
118
119 .. image:: images/custom-datetime-format.png
120 :alt: Screen shot of colored logging with custom date/time format.
121
122 Changing the colors/styles
123 --------------------------
124
125 Finally you can customize the colors and text styles that are used:
126
127 .. literalinclude:: examples/custom-colors.txt
128 :language: console
129
130 Here's an explanation of the features used here:
131
132 - The numbers used in ``$COLOREDLOGS_LEVEL_STYLES`` demonstrate the use of 256
133 color mode (the numbers refer to the 256 color mode palette which is fixed).
134
135 - The `success` level demonstrates the use of a text style (bold).
136
137 - The `critical` level demonstrates the use of a background color (red).
138
139 Of course none of this can be seen in the shell transcript quoted above, but
140 take a look at the following screen shot:
141
142 .. image:: images/custom-colors.png
143 :alt: Screen shot of colored logging with custom colors.
144
145 .. _notes about log levels:
146
147 Some notes about log levels
148 ===========================
149
150 With regards to the handling of log levels, the :mod:`coloredlogs` package
151 differs from Python's :mod:`logging` module in two aspects:
152
153 1. While the :mod:`logging` module uses the default logging level
154 :data:`logging.WARNING`, the :mod:`coloredlogs` package has always used
155 :data:`logging.INFO` as its default log level.
156
157 2. When logging to the terminal or system log is initialized by
158 :func:`install()` or :func:`.enable_system_logging()` the effective
159 level [#]_ of the selected logger [#]_ is compared against the requested
160 level [#]_ and if the effective level is more restrictive than the requested
161 level, the logger's level will be set to the requested level (this happens
162 in :func:`adjust_level()`). The reason for this is to work around a
163 combination of design choices in Python's :mod:`logging` module that can
164 easily confuse people who aren't already intimately familiar with it:
165
166 - All loggers are initialized with the level :data:`logging.NOTSET`.
167
168 - When a logger's level is set to :data:`logging.NOTSET` the
169 :func:`~logging.Logger.getEffectiveLevel()` method will
170 fall back to the level of the parent logger.
171
172 - The parent of all loggers is the root logger and the root logger has its
173 level set to :data:`logging.WARNING` by default (after importing the
174 :mod:`logging` module).
175
176 Effectively all user defined loggers inherit the default log level
177 :data:`logging.WARNING` from the root logger, which isn't very intuitive for
178 those who aren't already familiar with the hierarchical nature of the
179 :mod:`logging` module.
180
181 By avoiding this potentially confusing behavior (see `#14`_, `#18`_, `#21`_,
182 `#23`_ and `#24`_), while at the same time allowing the caller to specify a
183 logger object, my goal and hope is to provide sane defaults that can easily
184 be changed when the need arises.
185
186 .. [#] Refer to :func:`logging.Logger.getEffectiveLevel()` for details.
187 .. [#] The logger that is passed as an argument by the caller or the root
188 logger which is selected as a default when no logger is provided.
189 .. [#] The log level that is passed as an argument by the caller or the
190 default log level :data:`logging.INFO` when no level is provided.
191
192 .. _#14: https://github.com/xolox/python-coloredlogs/issues/14
193 .. _#18: https://github.com/xolox/python-coloredlogs/issues/18
194 .. _#21: https://github.com/xolox/python-coloredlogs/pull/21
195 .. _#23: https://github.com/xolox/python-coloredlogs/pull/23
196 .. _#24: https://github.com/xolox/python-coloredlogs/issues/24
197
198 Classes and functions
199 =====================
200 """
201
202 # Standard library modules.
203 import collections
204 import logging
205 import os
206 import re
207 import socket
208 import sys
209
210 # External dependencies.
211 from humanfriendly import coerce_boolean
212 from humanfriendly.compat import coerce_string, is_string, on_windows
213 from humanfriendly.terminal import ANSI_COLOR_CODES, ansi_wrap, enable_ansi_support, terminal_supports_colors
214 from humanfriendly.text import format, split
215
216 # Semi-standard module versioning.
217 __version__ = '15.0'
218
219 DEFAULT_LOG_LEVEL = logging.INFO
220 """The default log level for :mod:`coloredlogs` (:data:`logging.INFO`)."""
221
222 DEFAULT_LOG_FORMAT = '%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s'
223 """The default log format for :class:`ColoredFormatter` objects (a string)."""
224
225 DEFAULT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
226 """The default date/time format for :class:`ColoredFormatter` objects (a string)."""
227
228 CHROOT_FILES = ['/etc/debian_chroot']
229 """A list of filenames that indicate a chroot and contain the name of the chroot."""
230
231 DEFAULT_FIELD_STYLES = dict(
232 asctime=dict(color='green'),
233 hostname=dict(color='magenta'),
234 levelname=dict(color='black', bold=True),
235 name=dict(color='blue'),
236 programname=dict(color='cyan'),
237 username=dict(color='yellow'),
238 )
239 """Mapping of log format names to default font styles."""
240
241 DEFAULT_LEVEL_STYLES = dict(
242 spam=dict(color='green', faint=True),
243 debug=dict(color='green'),
244 verbose=dict(color='blue'),
245 info=dict(),
246 notice=dict(color='magenta'),
247 warning=dict(color='yellow'),
248 success=dict(color='green', bold=True),
249 error=dict(color='red'),
250 critical=dict(color='red', bold=True),
251 )
252 """Mapping of log level names to default font styles."""
253
254 DEFAULT_FORMAT_STYLE = '%'
255 """The default logging format style (a single character)."""
256
257 FORMAT_STYLE_PATTERNS = {
258 '%': r'%\((\w+)\)[#0 +-]*\d*(?:\.\d+)?[hlL]?[diouxXeEfFgGcrs%]',
259 '{': r'{(\w+)[^}]*}',
260 '$': r'\$(\w+)|\${(\w+)}',
261 }
262 """
263 A dictionary that maps the `style` characters ``%``, ``{`` and ``$`` (see the
264 documentation of the :class:`python3:logging.Formatter` class in Python 3.2+)
265 to strings containing regular expression patterns that can be used to parse
266 format strings in the corresponding style:
267
268 ``%``
269 A string containing a regular expression that matches a "percent conversion
270 specifier" as defined in the `String Formatting Operations`_ section of the
271 Python documentation. Here's an example of a logging format string in this
272 format: ``%(levelname)s:%(name)s:%(message)s``.
273
274 ``{``
275 A string containing a regular expression that matches a "replacement field" as
276 defined in the `Format String Syntax`_ section of the Python documentation.
277 Here's an example of a logging format string in this format:
278 ``{levelname}:{name}:{message}``.
279
280 ``$``
281 A string containing a regular expression that matches a "substitution
282 placeholder" as defined in the `Template Strings`_ section of the Python
283 documentation. Here's an example of a logging format string in this format:
284 ``$levelname:$name:$message``.
285
286 These regular expressions are used by :class:`FormatStringParser` to introspect
287 and manipulate logging format strings.
288
289 .. _String Formatting Operations: https://docs.python.org/2/library/stdtypes.html#string-formatting
290 .. _Format String Syntax: https://docs.python.org/2/library/string.html#formatstrings
291 .. _Template Strings: https://docs.python.org/3/library/string.html#template-strings
292 """
293
294
295 def auto_install():
296 """
297 Automatically call :func:`install()` when ``$COLOREDLOGS_AUTO_INSTALL`` is set.
298
299 The `coloredlogs` package includes a `path configuration file`_ that
300 automatically imports the :mod:`coloredlogs` module and calls
301 :func:`auto_install()` when the environment variable
302 ``$COLOREDLOGS_AUTO_INSTALL`` is set.
303
304 This function uses :func:`~humanfriendly.coerce_boolean()` to check whether
305 the value of ``$COLOREDLOGS_AUTO_INSTALL`` should be considered :data:`True`.
306
307 .. _path configuration file: https://docs.python.org/2/library/site.html#module-site
308 """
309 if coerce_boolean(os.environ.get('COLOREDLOGS_AUTO_INSTALL', 'false')):
310 install()
311
312
313 def install(level=None, **kw):
314 """
315 Enable colored terminal output for Python's :mod:`logging` module.
316
317 :param level: The default logging level (an integer or a string with a
318 level name, defaults to :data:`DEFAULT_LOG_LEVEL`).
319 :param logger: The logger to which the stream handler should be attached (a
320 :class:`~logging.Logger` object, defaults to the root logger).
321 :param fmt: Set the logging format (a string like those accepted by
322 :class:`~logging.Formatter`, defaults to
323 :data:`DEFAULT_LOG_FORMAT`).
324 :param datefmt: Set the date/time format (a string, defaults to
325 :data:`DEFAULT_DATE_FORMAT`).
326 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
327 :data:`DEFAULT_FORMAT_STYLE`). See the documentation of the
328 :class:`python3:logging.Formatter` class in Python 3.2+. On
329 older Python versions only ``%`` is supported.
330 :param milliseconds: :data:`True` to show milliseconds like :mod:`logging`
331 does by default, :data:`False` to hide milliseconds
332 (the default is :data:`False`, see `#16`_).
333 :param level_styles: A dictionary with custom level styles (defaults to
334 :data:`DEFAULT_LEVEL_STYLES`).
335 :param field_styles: A dictionary with custom field styles (defaults to
336 :data:`DEFAULT_FIELD_STYLES`).
337 :param stream: The stream where log messages should be written to (a
338 file-like object). This defaults to :data:`None` which
339 means :class:`StandardErrorHandler` is used.
340 :param isatty: :data:`True` to use a :class:`ColoredFormatter`,
341 :data:`False` to use a normal :class:`~logging.Formatter`
342 (defaults to auto-detection using
343 :func:`~humanfriendly.terminal.terminal_supports_colors()`).
344 :param reconfigure: If :data:`True` (the default) multiple calls to
345 :func:`coloredlogs.install()` will each override
346 the previous configuration.
347 :param use_chroot: Refer to :class:`HostNameFilter`.
348 :param programname: Refer to :class:`ProgramNameFilter`.
349 :param username: Refer to :class:`UserNameFilter`.
350 :param syslog: If :data:`True` then :func:`.enable_system_logging()` will
351 be called without arguments (defaults to :data:`False`). The
352 `syslog` argument may also be a number or string, in this
353 case it is assumed to be a logging level which is passed on
354 to :func:`.enable_system_logging()`.
355
356 The :func:`coloredlogs.install()` function is similar to
357 :func:`logging.basicConfig()`, both functions take a lot of optional
358 keyword arguments but try to do the right thing by default:
359
360 1. If `reconfigure` is :data:`True` (it is by default) and an existing
361 :class:`~logging.StreamHandler` is found that is connected to either
362 :data:`~sys.stdout` or :data:`~sys.stderr` the handler will be removed.
363 This means that first calling :func:`logging.basicConfig()` and then
364 calling :func:`coloredlogs.install()` will replace the stream handler
365 instead of adding a duplicate stream handler. If `reconfigure` is
366 :data:`False` and an existing handler is found no further steps are
367 taken (to avoid installing a duplicate stream handler).
368
369 2. A :class:`~logging.StreamHandler` is created and connected to the stream
370 given by the `stream` keyword argument (:data:`sys.stderr` by
371 default). The stream handler's level is set to the value of the `level`
372 keyword argument.
373
374 3. A :class:`ColoredFormatter` is created if the `isatty` keyword argument
375 allows it (or auto-detection allows it), otherwise a normal
376 :class:`~logging.Formatter` is created. The formatter is initialized
377 with the `fmt` and `datefmt` keyword arguments (or their computed
378 defaults).
379
380 The environment variable ``$NO_COLOR`` is taken as a hint by
381 auto-detection that colors should not be used.
382
383 4. :func:`HostNameFilter.install()`, :func:`ProgramNameFilter.install()`
384 and :func:`UserNameFilter.install()` are called to enable the use of
385 additional fields in the log format.
386
387 5. If the logger's level is too restrictive it is relaxed (refer to `notes
388 about log levels`_ for details).
389
390 6. The formatter is added to the handler and the handler is added to the
391 logger.
392
393 .. _#16: https://github.com/xolox/python-coloredlogs/issues/16
394 """
395 logger = kw.get('logger') or logging.getLogger()
396 reconfigure = kw.get('reconfigure', True)
397 stream = kw.get('stream', sys.stderr)
398 style = check_style(kw.get('style') or DEFAULT_FORMAT_STYLE)
399 # Get the log level from an argument, environment variable or default and
400 # convert the names of log levels to numbers to enable numeric comparison.
401 if level is None:
402 level = os.environ.get('COLOREDLOGS_LOG_LEVEL', DEFAULT_LOG_LEVEL)
403 level = level_to_number(level)
404 # Remove any existing stream handler that writes to stdout or stderr, even
405 # if the stream handler wasn't created by coloredlogs because multiple
406 # stream handlers (in the same hierarchy) writing to stdout or stderr would
407 # create duplicate output. `None' is a synonym for the possibly dynamic
408 # value of the stderr attribute of the sys module.
409 match_streams = ([sys.stdout, sys.stderr]
410 if stream in [sys.stdout, sys.stderr, None]
411 else [stream])
412 match_handler = lambda handler: match_stream_handler(handler, match_streams)
413 handler, logger = replace_handler(logger, match_handler, reconfigure)
414 # Make sure reconfiguration is allowed or not relevant.
415 if not (handler and not reconfigure):
416 # Make it easy to enable system logging.
417 syslog_enabled = kw.get('syslog')
418 # We ignore the value `None' because it means the caller didn't opt in
419 # to system logging and `False' because it means the caller explicitly
420 # opted out of system logging.
421 if syslog_enabled not in (None, False):
422 from coloredlogs.syslog import enable_system_logging
423 if syslog_enabled is True:
424 # If the caller passed syslog=True then we leave the choice of
425 # default log level up to the coloredlogs.syslog module.
426 enable_system_logging()
427 else:
428 # Values other than (None, True, False) are assumed to
429 # represent a logging level for system logging.
430 enable_system_logging(level=syslog_enabled)
431 # Figure out whether we can use ANSI escape sequences.
432 use_colors = kw.get('isatty', None)
433 # In the following indented block the expression (use_colors is None)
434 # can be read as "auto detect is enabled and no reason has yet been
435 # found to automatically disable color support".
436 if use_colors or (use_colors is None):
437 # Respect the user's choice not to have colors.
438 if use_colors is None and 'NO_COLOR' in os.environ:
439 # For details on this see https://no-color.org/.
440 use_colors = False
441 # Try to enable Windows native ANSI support or Colorama?
442 if (use_colors or use_colors is None) and on_windows():
443 # This can fail, in which case ANSI escape sequences would end
444 # up being printed to the terminal in raw form. This is very
445 # user hostile, so to avoid this happening we disable color
446 # support on failure.
447 use_colors = enable_ansi_support()
448 # When auto detection is enabled, and so far we encountered no
449 # reason to disable color support, then we will enable color
450 # support if 'stream' is connected to a terminal.
451 if use_colors is None:
452 use_colors = terminal_supports_colors(stream)
453 # Create a stream handler and make sure to preserve any filters
454 # the current handler may have (if an existing handler is found).
455 filters = handler.filters if handler else None
456 handler = logging.StreamHandler(stream) if stream else StandardErrorHandler()
457 handler.setLevel(level)
458 if filters:
459 handler.filters = filters
460 # Prepare the arguments to the formatter, allowing the caller to
461 # customize the values of `fmt', `datefmt' and `style' as desired.
462 formatter_options = dict(fmt=kw.get('fmt'), datefmt=kw.get('datefmt'))
463 # Only pass the `style' argument to the formatter when the caller
464 # provided an alternative logging format style. This prevents
465 # TypeError exceptions on Python versions before 3.2.
466 if style != DEFAULT_FORMAT_STYLE:
467 formatter_options['style'] = style
468 # Come up with a default log format?
469 if not formatter_options['fmt']:
470 # Use the log format defined by the environment variable
471 # $COLOREDLOGS_LOG_FORMAT or fall back to the default.
472 formatter_options['fmt'] = os.environ.get('COLOREDLOGS_LOG_FORMAT') or DEFAULT_LOG_FORMAT
473 # If the caller didn't specify a date/time format we'll use the format
474 # defined by the environment variable $COLOREDLOGS_DATE_FORMAT (or fall
475 # back to the default).
476 if not formatter_options['datefmt']:
477 formatter_options['datefmt'] = os.environ.get('COLOREDLOGS_DATE_FORMAT') or DEFAULT_DATE_FORMAT
478 # Python's logging module shows milliseconds by default through special
479 # handling in the logging.Formatter.formatTime() method [1]. Because
480 # coloredlogs always defines a `datefmt' it bypasses this special
481 # handling, which is fine because ever since publishing coloredlogs
482 # I've never needed millisecond precision ;-). However there are users
483 # of coloredlogs that do want milliseconds to be shown [2] so we
484 # provide a shortcut to make it easy.
485 #
486 # [1] https://stackoverflow.com/questions/6290739/python-logging-use-milliseconds-in-time-format
487 # [2] https://github.com/xolox/python-coloredlogs/issues/16
488 if kw.get('milliseconds'):
489 parser = FormatStringParser(style=style)
490 if not (parser.contains_field(formatter_options['fmt'], 'msecs')
491 or '%f' in formatter_options['datefmt']):
492 pattern = parser.get_pattern('asctime')
493 replacements = {'%': '%(msecs)03d', '{': '{msecs:03}', '$': '${msecs}'}
494 formatter_options['fmt'] = pattern.sub(
495 r'\g<0>,' + replacements[style],
496 formatter_options['fmt'],
497 )
498 # Do we need to make %(hostname) available to the formatter?
499 HostNameFilter.install(
500 fmt=formatter_options['fmt'],
501 handler=handler,
502 style=style,
503 use_chroot=kw.get('use_chroot', True),
504 )
505 # Do we need to make %(programname) available to the formatter?
506 ProgramNameFilter.install(
507 fmt=formatter_options['fmt'],
508 handler=handler,
509 programname=kw.get('programname'),
510 style=style,
511 )
512 # Do we need to make %(username) available to the formatter?
513 UserNameFilter.install(
514 fmt=formatter_options['fmt'],
515 handler=handler,
516 username=kw.get('username'),
517 style=style,
518 )
519 # Inject additional formatter arguments specific to ColoredFormatter?
520 if use_colors:
521 for name, environment_name in (('field_styles', 'COLOREDLOGS_FIELD_STYLES'),
522 ('level_styles', 'COLOREDLOGS_LEVEL_STYLES')):
523 value = kw.get(name)
524 if value is None:
525 # If no styles have been specified we'll fall back
526 # to the styles defined by the environment variable.
527 environment_value = os.environ.get(environment_name)
528 if environment_value is not None:
529 value = parse_encoded_styles(environment_value)
530 if value is not None:
531 formatter_options[name] = value
532 # Create a (possibly colored) formatter.
533 formatter_type = ColoredFormatter if use_colors else BasicFormatter
534 handler.setFormatter(formatter_type(**formatter_options))
535 # Adjust the level of the selected logger.
536 adjust_level(logger, level)
537 # Install the stream handler.
538 logger.addHandler(handler)
539
540
541 def check_style(value):
542 """
543 Validate a logging format style.
544
545 :param value: The logging format style to validate (any value).
546 :returns: The logging format character (a string of one character).
547 :raises: :exc:`~exceptions.ValueError` when the given style isn't supported.
548
549 On Python 3.2+ this function accepts the logging format styles ``%``, ``{``
550 and ``$`` while on older versions only ``%`` is accepted (because older
551 Python versions don't support alternative logging format styles).
552 """
553 if sys.version_info[:2] >= (3, 2):
554 if value not in FORMAT_STYLE_PATTERNS:
555 msg = "Unsupported logging format style! (%r)"
556 raise ValueError(format(msg, value))
557 elif value != DEFAULT_FORMAT_STYLE:
558 msg = "Format string styles other than %r require Python 3.2+!"
559 raise ValueError(msg, DEFAULT_FORMAT_STYLE)
560 return value
561
562
563 def increase_verbosity():
564 """
565 Increase the verbosity of the root handler by one defined level.
566
567 Understands custom logging levels like defined by my ``verboselogs``
568 module.
569 """
570 defined_levels = sorted(set(find_defined_levels().values()))
571 current_index = defined_levels.index(get_level())
572 selected_index = max(0, current_index - 1)
573 set_level(defined_levels[selected_index])
574
575
576 def decrease_verbosity():
577 """
578 Decrease the verbosity of the root handler by one defined level.
579
580 Understands custom logging levels like defined by my ``verboselogs``
581 module.
582 """
583 defined_levels = sorted(set(find_defined_levels().values()))
584 current_index = defined_levels.index(get_level())
585 selected_index = min(current_index + 1, len(defined_levels) - 1)
586 set_level(defined_levels[selected_index])
587
588
589 def is_verbose():
590 """
591 Check whether the log level of the root handler is set to a verbose level.
592
593 :returns: ``True`` if the root handler is verbose, ``False`` if not.
594 """
595 return get_level() < DEFAULT_LOG_LEVEL
596
597
598 def get_level():
599 """
600 Get the logging level of the root handler.
601
602 :returns: The logging level of the root handler (an integer) or
603 :data:`DEFAULT_LOG_LEVEL` (if no root handler exists).
604 """
605 handler, logger = find_handler(logging.getLogger(), match_stream_handler)
606 return handler.level if handler else DEFAULT_LOG_LEVEL
607
608
609 def set_level(level):
610 """
611 Set the logging level of the root handler.
612
613 :param level: The logging level to filter on (an integer or string).
614
615 If no root handler exists yet this automatically calls :func:`install()`.
616 """
617 handler, logger = find_handler(logging.getLogger(), match_stream_handler)
618 if handler and logger:
619 # Change the level of the existing handler.
620 handler.setLevel(level_to_number(level))
621 # Adjust the level of the selected logger.
622 adjust_level(logger, level)
623 else:
624 # Create a new handler with the given level.
625 install(level=level)
626
627
628 def adjust_level(logger, level):
629 """
630 Increase a logger's verbosity up to the requested level.
631
632 :param logger: The logger to change (a :class:`~logging.Logger` object).
633 :param level: The log level to enable (a string or number).
634
635 This function is used by functions like :func:`install()`,
636 :func:`increase_verbosity()` and :func:`.enable_system_logging()` to adjust
637 a logger's level so that log messages up to the requested log level are
638 propagated to the configured output handler(s).
639
640 It uses :func:`logging.Logger.getEffectiveLevel()` to check whether
641 `logger` propagates or swallows log messages of the requested `level` and
642 sets the logger's level to the requested level if it would otherwise
643 swallow log messages.
644
645 Effectively this function will "widen the scope of logging" when asked to
646 do so but it will never "narrow the scope of logging". This is because I am
647 convinced that filtering of log messages should (primarily) be decided by
648 handlers.
649 """
650 level = level_to_number(level)
651 if logger.getEffectiveLevel() > level:
652 logger.setLevel(level)
653
654
655 def find_defined_levels():
656 """
657 Find the defined logging levels.
658
659 :returns: A dictionary with level names as keys and integers as values.
660
661 Here's what the result looks like by default (when
662 no custom levels or level names have been defined):
663
664 >>> find_defined_levels()
665 {'NOTSET': 0,
666 'DEBUG': 10,
667 'INFO': 20,
668 'WARN': 30,
669 'WARNING': 30,
670 'ERROR': 40,
671 'FATAL': 50,
672 'CRITICAL': 50}
673 """
674 defined_levels = {}
675 for name in dir(logging):
676 if name.isupper():
677 value = getattr(logging, name)
678 if isinstance(value, int):
679 defined_levels[name] = value
680 return defined_levels
681
682
683 def level_to_number(value):
684 """
685 Coerce a logging level name to a number.
686
687 :param value: A logging level (integer or string).
688 :returns: The number of the log level (an integer).
689
690 This function translates log level names into their numeric values..
691 """
692 if is_string(value):
693 try:
694 defined_levels = find_defined_levels()
695 value = defined_levels[value.upper()]
696 except KeyError:
697 # Don't fail on unsupported log levels.
698 value = DEFAULT_LOG_LEVEL
699 return value
700
701
702 def find_level_aliases():
703 """
704 Find log level names which are aliases of each other.
705
706 :returns: A dictionary that maps aliases to their canonical name.
707
708 .. note:: Canonical names are chosen to be the alias with the longest
709 string length so that e.g. ``WARN`` is an alias for ``WARNING``
710 instead of the other way around.
711
712 Here's what the result looks like by default (when
713 no custom levels or level names have been defined):
714
715 >>> from coloredlogs import find_level_aliases
716 >>> find_level_aliases()
717 {'WARN': 'WARNING', 'FATAL': 'CRITICAL'}
718 """
719 mapping = collections.defaultdict(list)
720 for name, value in find_defined_levels().items():
721 mapping[value].append(name)
722 aliases = {}
723 for value, names in mapping.items():
724 if len(names) > 1:
725 names = sorted(names, key=lambda n: len(n))
726 canonical_name = names.pop()
727 for alias in names:
728 aliases[alias] = canonical_name
729 return aliases
730
731
732 def parse_encoded_styles(text, normalize_key=None):
733 """
734 Parse text styles encoded in a string into a nested data structure.
735
736 :param text: The encoded styles (a string).
737 :returns: A dictionary in the structure of the :data:`DEFAULT_FIELD_STYLES`
738 and :data:`DEFAULT_LEVEL_STYLES` dictionaries.
739
740 Here's an example of how this function works:
741
742 >>> from coloredlogs import parse_encoded_styles
743 >>> from pprint import pprint
744 >>> encoded_styles = 'debug=green;warning=yellow;error=red;critical=red,bold'
745 >>> pprint(parse_encoded_styles(encoded_styles))
746 {'debug': {'color': 'green'},
747 'warning': {'color': 'yellow'},
748 'error': {'color': 'red'},
749 'critical': {'bold': True, 'color': 'red'}}
750 """
751 parsed_styles = {}
752 for assignment in split(text, ';'):
753 name, _, styles = assignment.partition('=')
754 target = parsed_styles.setdefault(name, {})
755 for token in split(styles, ','):
756 # When this code was originally written, setting background colors
757 # wasn't supported yet, so there was no need to disambiguate
758 # between the text color and background color. This explains why
759 # a color name or number implies setting the text color (for
760 # backwards compatibility).
761 if token.isdigit():
762 target['color'] = int(token)
763 elif token in ANSI_COLOR_CODES:
764 target['color'] = token
765 elif '=' in token:
766 name, _, value = token.partition('=')
767 if name in ('color', 'background'):
768 if value.isdigit():
769 target[name] = int(value)
770 elif value in ANSI_COLOR_CODES:
771 target[name] = value
772 else:
773 target[token] = True
774 return parsed_styles
775
776
777 def find_hostname(use_chroot=True):
778 """
779 Find the host name to include in log messages.
780
781 :param use_chroot: Use the name of the chroot when inside a chroot?
782 (boolean, defaults to :data:`True`)
783 :returns: A suitable host name (a string).
784
785 Looks for :data:`CHROOT_FILES` that have a nonempty first line (taken to be
786 the chroot name). If none are found then :func:`socket.gethostname()` is
787 used as a fall back.
788 """
789 for chroot_file in CHROOT_FILES:
790 try:
791 with open(chroot_file) as handle:
792 first_line = next(handle)
793 name = first_line.strip()
794 if name:
795 return name
796 except Exception:
797 pass
798 return socket.gethostname()
799
800
801 def find_program_name():
802 """
803 Select a suitable program name to embed in log messages.
804
805 :returns: One of the following strings (in decreasing order of preference):
806
807 1. The base name of the currently running Python program or
808 script (based on the value at index zero of :data:`sys.argv`).
809 2. The base name of the Python executable (based on
810 :data:`sys.executable`).
811 3. The string 'python'.
812 """
813 # Gotcha: sys.argv[0] is '-c' if Python is started with the -c option.
814 return ((os.path.basename(sys.argv[0]) if sys.argv and sys.argv[0] != '-c' else '')
815 or (os.path.basename(sys.executable) if sys.executable else '')
816 or 'python')
817
818
819 def find_username():
820 """
821 Find the username to include in log messages.
822
823 :returns: A suitable username (a string).
824
825 On UNIX systems this uses the :mod:`pwd` module which means ``root`` will
826 be reported when :man:`sudo` is used (as it should). If this fails (for
827 example on Windows) then :func:`getpass.getuser()` is used as a fall back.
828 """
829 try:
830 import pwd
831 uid = os.getuid()
832 entry = pwd.getpwuid(uid)
833 return entry.pw_name
834 except Exception:
835 import getpass
836 return getpass.getuser()
837
838
839 def replace_handler(logger, match_handler, reconfigure):
840 """
841 Prepare to replace a handler.
842
843 :param logger: Refer to :func:`find_handler()`.
844 :param match_handler: Refer to :func:`find_handler()`.
845 :param reconfigure: :data:`True` if an existing handler should be replaced,
846 :data:`False` otherwise.
847 :returns: A tuple of two values:
848
849 1. The matched :class:`~logging.Handler` object or :data:`None`
850 if no handler was matched.
851 2. The :class:`~logging.Logger` to which the matched handler was
852 attached or the logger given to :func:`replace_handler()`.
853 """
854 handler, other_logger = find_handler(logger, match_handler)
855 if handler and other_logger and reconfigure:
856 # Remove the existing handler from the logger that its attached to
857 # so that we can install a new handler that behaves differently.
858 other_logger.removeHandler(handler)
859 # Switch to the logger that the existing handler was attached to so
860 # that reconfiguration doesn't narrow the scope of logging.
861 logger = other_logger
862 return handler, logger
863
864
865 def find_handler(logger, match_handler):
866 """
867 Find a (specific type of) handler in the propagation tree of a logger.
868
869 :param logger: The logger to check (a :class:`~logging.Logger` object).
870 :param match_handler: A callable that receives a :class:`~logging.Handler`
871 object and returns :data:`True` to match a handler or
872 :data:`False` to skip that handler and continue
873 searching for a match.
874 :returns: A tuple of two values:
875
876 1. The matched :class:`~logging.Handler` object or :data:`None`
877 if no handler was matched.
878 2. The :class:`~logging.Logger` object to which the handler is
879 attached or :data:`None` if no handler was matched.
880
881 This function finds a logging handler (of the given type) attached to a
882 logger or one of its parents (see :func:`walk_propagation_tree()`). It uses
883 the undocumented :class:`~logging.Logger.handlers` attribute to find
884 handlers attached to a logger, however it won't raise an exception if the
885 attribute isn't available. The advantages of this approach are:
886
887 - This works regardless of whether :mod:`coloredlogs` attached the handler
888 or other Python code attached the handler.
889
890 - This will correctly recognize the situation where the given logger has no
891 handlers but :attr:`~logging.Logger.propagate` is enabled and the logger
892 has a parent logger that does have a handler attached.
893 """
894 for logger in walk_propagation_tree(logger):
895 for handler in getattr(logger, 'handlers', []):
896 if match_handler(handler):
897 return handler, logger
898 return None, None
899
900
901 def match_stream_handler(handler, streams=[]):
902 """
903 Identify stream handlers writing to the given streams(s).
904
905 :param handler: The :class:`~logging.Handler` class to check.
906 :param streams: A sequence of streams to match (defaults to matching
907 :data:`~sys.stdout` and :data:`~sys.stderr`).
908 :returns: :data:`True` if the handler is a :class:`~logging.StreamHandler`
909 logging to the given stream(s), :data:`False` otherwise.
910
911 This function can be used as a callback for :func:`find_handler()`.
912 """
913 return (isinstance(handler, logging.StreamHandler)
914 and getattr(handler, 'stream') in (streams or (sys.stdout, sys.stderr)))
915
916
917 def walk_propagation_tree(logger):
918 """
919 Walk through the propagation hierarchy of the given logger.
920
921 :param logger: The logger whose hierarchy to walk (a
922 :class:`~logging.Logger` object).
923 :returns: A generator of :class:`~logging.Logger` objects.
924
925 .. note:: This uses the undocumented :class:`logging.Logger.parent`
926 attribute to find higher level loggers, however it won't
927 raise an exception if the attribute isn't available.
928 """
929 while isinstance(logger, logging.Logger):
930 # Yield the logger to our caller.
931 yield logger
932 # Check if the logger has propagation enabled.
933 if logger.propagate:
934 # Continue with the parent logger. We use getattr() because the
935 # `parent' attribute isn't documented so properly speaking we
936 # shouldn't break if it's not available.
937 logger = getattr(logger, 'parent', None)
938 else:
939 # The propagation chain stops here.
940 logger = None
941
942
943 class BasicFormatter(logging.Formatter):
944
945 """
946 Log :class:`~logging.Formatter` that supports ``%f`` for millisecond formatting.
947
948 This class extends :class:`~logging.Formatter` to enable the use of ``%f``
949 for millisecond formatting in date/time strings, to allow for the type of
950 flexibility requested in issue `#45`_.
951
952 .. _#45: https://github.com/xolox/python-coloredlogs/issues/45
953 """
954
955 def formatTime(self, record, datefmt=None):
956 """
957 Format the date/time of a log record.
958
959 :param record: A :class:`~logging.LogRecord` object.
960 :param datefmt: A date/time format string (defaults to :data:`DEFAULT_DATE_FORMAT`).
961 :returns: The formatted date/time (a string).
962
963 This method overrides :func:`~logging.Formatter.formatTime()` to set
964 `datefmt` to :data:`DEFAULT_DATE_FORMAT` when the caller hasn't
965 specified a date format.
966
967 When `datefmt` contains the token ``%f`` it will be replaced by the
968 value of ``%(msecs)03d`` (refer to issue `#45`_ for use cases).
969 """
970 # The default value of the following argument is defined here so
971 # that Sphinx doesn't embed the default value in the generated
972 # documentation (because the result is awkward to read).
973 datefmt = datefmt or DEFAULT_DATE_FORMAT
974 # Replace %f with the value of %(msecs)03d.
975 if '%f' in datefmt:
976 datefmt = datefmt.replace('%f', '%03d' % record.msecs)
977 # Delegate the actual date/time formatting to the base formatter.
978 return logging.Formatter.formatTime(self, record, datefmt)
979
980
981 class ColoredFormatter(BasicFormatter):
982
983 """
984 Log :class:`~logging.Formatter` that uses `ANSI escape sequences`_ to create colored logs.
985
986 :class:`ColoredFormatter` inherits from :class:`BasicFormatter` to enable
987 the use of ``%f`` for millisecond formatting in date/time strings.
988
989 .. note:: If you want to use :class:`ColoredFormatter` on Windows then you
990 need to call :func:`~humanfriendly.terminal.enable_ansi_support()`.
991 This is done for you when you call :func:`coloredlogs.install()`.
992 """
993
994 def __init__(self, fmt=None, datefmt=None, style=DEFAULT_FORMAT_STYLE, level_styles=None, field_styles=None):
995 """
996 Initialize a :class:`ColoredFormatter` object.
997
998 :param fmt: A log format string (defaults to :data:`DEFAULT_LOG_FORMAT`).
999 :param datefmt: A date/time format string (defaults to :data:`None`,
1000 but see the documentation of
1001 :func:`BasicFormatter.formatTime()`).
1002 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
1003 :data:`DEFAULT_FORMAT_STYLE`)
1004 :param level_styles: A dictionary with custom level styles
1005 (defaults to :data:`DEFAULT_LEVEL_STYLES`).
1006 :param field_styles: A dictionary with custom field styles
1007 (defaults to :data:`DEFAULT_FIELD_STYLES`).
1008 :raises: Refer to :func:`check_style()`.
1009
1010 This initializer uses :func:`colorize_format()` to inject ANSI escape
1011 sequences in the log format string before it is passed to the
1012 initializer of the base class.
1013 """
1014 self.nn = NameNormalizer()
1015 # The default values of the following arguments are defined here so
1016 # that Sphinx doesn't embed the default values in the generated
1017 # documentation (because the result is awkward to read).
1018 fmt = fmt or DEFAULT_LOG_FORMAT
1019 self.level_styles = self.nn.normalize_keys(DEFAULT_LEVEL_STYLES if level_styles is None else level_styles)
1020 self.field_styles = self.nn.normalize_keys(DEFAULT_FIELD_STYLES if field_styles is None else field_styles)
1021 # Rewrite the format string to inject ANSI escape sequences.
1022 kw = dict(fmt=self.colorize_format(fmt, style), datefmt=datefmt)
1023 # If we were given a non-default logging format style we pass it on
1024 # to our superclass. At this point check_style() will have already
1025 # complained that the use of alternative logging format styles
1026 # requires Python 3.2 or newer.
1027 if style != DEFAULT_FORMAT_STYLE:
1028 kw['style'] = style
1029 # Initialize the superclass with the rewritten format string.
1030 logging.Formatter.__init__(self, **kw)
1031
1032 def colorize_format(self, fmt, style=DEFAULT_FORMAT_STYLE):
1033 """
1034 Rewrite a logging format string to inject ANSI escape sequences.
1035
1036 :param fmt: The log format string.
1037 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
1038 :data:`DEFAULT_FORMAT_STYLE`).
1039 :returns: The logging format string with ANSI escape sequences.
1040
1041 This method takes a logging format string like the ones you give to
1042 :class:`logging.Formatter` and processes it as follows:
1043
1044 1. First the logging format string is separated into formatting
1045 directives versus surrounding text (according to the given `style`).
1046
1047 2. Then formatting directives and surrounding text are grouped
1048 based on whitespace delimiters (in the surrounding text).
1049
1050 3. For each group styling is selected as follows:
1051
1052 1. If the group contains a single formatting directive that has
1053 a style defined then the whole group is styled accordingly.
1054
1055 2. If the group contains multiple formatting directives that
1056 have styles defined then each formatting directive is styled
1057 individually and surrounding text isn't styled.
1058
1059 As an example consider the default log format (:data:`DEFAULT_LOG_FORMAT`)::
1060
1061 %(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s
1062
1063 The default field styles (:data:`DEFAULT_FIELD_STYLES`) define a style for the
1064 `name` field but not for the `process` field, however because both fields
1065 are part of the same whitespace delimited token they'll be highlighted
1066 together in the style defined for the `name` field.
1067 """
1068 result = []
1069 parser = FormatStringParser(style=style)
1070 for group in parser.get_grouped_pairs(fmt):
1071 applicable_styles = [self.nn.get(self.field_styles, token.name) for token in group if token.name]
1072 if sum(map(bool, applicable_styles)) == 1:
1073 # If exactly one (1) field style is available for the group of
1074 # tokens then all of the tokens will be styled the same way.
1075 # This provides a limited form of backwards compatibility with
1076 # the (intended) behavior of coloredlogs before the release of
1077 # version 10.
1078 result.append(ansi_wrap(
1079 ''.join(token.text for token in group),
1080 **next(s for s in applicable_styles if s)
1081 ))
1082 else:
1083 for token in group:
1084 text = token.text
1085 if token.name:
1086 field_styles = self.nn.get(self.field_styles, token.name)
1087 if field_styles:
1088 text = ansi_wrap(text, **field_styles)
1089 result.append(text)
1090 return ''.join(result)
1091
1092 def format(self, record):
1093 """
1094 Apply level-specific styling to log records.
1095
1096 :param record: A :class:`~logging.LogRecord` object.
1097 :returns: The result of :func:`logging.Formatter.format()`.
1098
1099 This method injects ANSI escape sequences that are specific to the
1100 level of each log record (because such logic cannot be expressed in the
1101 syntax of a log format string). It works by making a copy of the log
1102 record, changing the `msg` field inside the copy and passing the copy
1103 into the :func:`~logging.Formatter.format()` method of the base
1104 class.
1105 """
1106 style = self.nn.get(self.level_styles, record.levelname)
1107 # After the introduction of the `Empty' class it was reported in issue
1108 # 33 that format() can be called when `Empty' has already been garbage
1109 # collected. This explains the (otherwise rather out of place) `Empty
1110 # is not None' check in the following `if' statement. The reasoning
1111 # here is that it's much better to log a message without formatting
1112 # then to raise an exception ;-).
1113 #
1114 # For more details refer to issue 33 on GitHub:
1115 # https://github.com/xolox/python-coloredlogs/issues/33
1116 if style and Empty is not None:
1117 # Due to the way that Python's logging module is structured and
1118 # documented the only (IMHO) clean way to customize its behavior is
1119 # to change incoming LogRecord objects before they get to the base
1120 # formatter. However we don't want to break other formatters and
1121 # handlers, so we copy the log record.
1122 #
1123 # In the past this used copy.copy() but as reported in issue 29
1124 # (which is reproducible) this can cause deadlocks. The following
1125 # Python voodoo is intended to accomplish the same thing as
1126 # copy.copy() without all of the generalization and overhead that
1127 # we don't need for our -very limited- use case.
1128 #
1129 # For more details refer to issue 29 on GitHub:
1130 # https://github.com/xolox/python-coloredlogs/issues/29
1131 copy = Empty()
1132 copy.__class__ = record.__class__
1133 copy.__dict__.update(record.__dict__)
1134 copy.msg = ansi_wrap(coerce_string(record.msg), **style)
1135 record = copy
1136 # Delegate the remaining formatting to the base formatter.
1137 return logging.Formatter.format(self, record)
1138
1139
1140 class Empty(object):
1141 """An empty class used to copy :class:`~logging.LogRecord` objects without reinitializing them."""
1142
1143
1144 class HostNameFilter(logging.Filter):
1145
1146 """
1147 Log filter to enable the ``%(hostname)s`` format.
1148
1149 Python's :mod:`logging` module doesn't expose the system's host name while
1150 I consider this to be a valuable addition. Fortunately it's very easy to
1151 expose additional fields in format strings: :func:`filter()` simply sets
1152 the ``hostname`` attribute of each :class:`~logging.LogRecord` object it
1153 receives and this is enough to enable the use of the ``%(hostname)s``
1154 expression in format strings.
1155
1156 You can install this log filter as follows::
1157
1158 >>> import coloredlogs, logging
1159 >>> handler = logging.StreamHandler()
1160 >>> handler.addFilter(coloredlogs.HostNameFilter())
1161 >>> handler.setFormatter(logging.Formatter('[%(hostname)s] %(message)s'))
1162 >>> logger = logging.getLogger()
1163 >>> logger.addHandler(handler)
1164 >>> logger.setLevel(logging.INFO)
1165 >>> logger.info("Does it work?")
1166 [peter-macbook] Does it work?
1167
1168 Of course :func:`coloredlogs.install()` does all of this for you :-).
1169 """
1170
1171 @classmethod
1172 def install(cls, handler, fmt=None, use_chroot=True, style=DEFAULT_FORMAT_STYLE):
1173 """
1174 Install the :class:`HostNameFilter` on a log handler (only if needed).
1175
1176 :param fmt: The log format string to check for ``%(hostname)``.
1177 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
1178 :data:`DEFAULT_FORMAT_STYLE`).
1179 :param handler: The logging handler on which to install the filter.
1180 :param use_chroot: Refer to :func:`find_hostname()`.
1181
1182 If `fmt` is given the filter will only be installed if `fmt` uses the
1183 ``hostname`` field. If `fmt` is not given the filter is installed
1184 unconditionally.
1185 """
1186 if fmt:
1187 parser = FormatStringParser(style=style)
1188 if not parser.contains_field(fmt, 'hostname'):
1189 return
1190 handler.addFilter(cls(use_chroot))
1191
1192 def __init__(self, use_chroot=True):
1193 """
1194 Initialize a :class:`HostNameFilter` object.
1195
1196 :param use_chroot: Refer to :func:`find_hostname()`.
1197 """
1198 self.hostname = find_hostname(use_chroot)
1199
1200 def filter(self, record):
1201 """Set each :class:`~logging.LogRecord`'s `hostname` field."""
1202 # Modify the record.
1203 record.hostname = self.hostname
1204 # Don't filter the record.
1205 return 1
1206
1207
1208 class ProgramNameFilter(logging.Filter):
1209
1210 """
1211 Log filter to enable the ``%(programname)s`` format.
1212
1213 Python's :mod:`logging` module doesn't expose the name of the currently
1214 running program while I consider this to be a useful addition. Fortunately
1215 it's very easy to expose additional fields in format strings:
1216 :func:`filter()` simply sets the ``programname`` attribute of each
1217 :class:`~logging.LogRecord` object it receives and this is enough to enable
1218 the use of the ``%(programname)s`` expression in format strings.
1219
1220 Refer to :class:`HostNameFilter` for an example of how to manually install
1221 these log filters.
1222 """
1223
1224 @classmethod
1225 def install(cls, handler, fmt, programname=None, style=DEFAULT_FORMAT_STYLE):
1226 """
1227 Install the :class:`ProgramNameFilter` (only if needed).
1228
1229 :param fmt: The log format string to check for ``%(programname)``.
1230 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
1231 :data:`DEFAULT_FORMAT_STYLE`).
1232 :param handler: The logging handler on which to install the filter.
1233 :param programname: Refer to :func:`__init__()`.
1234
1235 If `fmt` is given the filter will only be installed if `fmt` uses the
1236 ``programname`` field. If `fmt` is not given the filter is installed
1237 unconditionally.
1238 """
1239 if fmt:
1240 parser = FormatStringParser(style=style)
1241 if not parser.contains_field(fmt, 'programname'):
1242 return
1243 handler.addFilter(cls(programname))
1244
1245 def __init__(self, programname=None):
1246 """
1247 Initialize a :class:`ProgramNameFilter` object.
1248
1249 :param programname: The program name to use (defaults to the result of
1250 :func:`find_program_name()`).
1251 """
1252 self.programname = programname or find_program_name()
1253
1254 def filter(self, record):
1255 """Set each :class:`~logging.LogRecord`'s `programname` field."""
1256 # Modify the record.
1257 record.programname = self.programname
1258 # Don't filter the record.
1259 return 1
1260
1261
1262 class UserNameFilter(logging.Filter):
1263
1264 """
1265 Log filter to enable the ``%(username)s`` format.
1266
1267 Python's :mod:`logging` module doesn't expose the username of the currently
1268 logged in user as requested in `#76`_. Given that :class:`HostNameFilter`
1269 and :class:`ProgramNameFilter` are already provided by `coloredlogs` it
1270 made sense to provide :class:`UserNameFilter` as well.
1271
1272 Refer to :class:`HostNameFilter` for an example of how to manually install
1273 these log filters.
1274
1275 .. _#76: https://github.com/xolox/python-coloredlogs/issues/76
1276 """
1277
1278 @classmethod
1279 def install(cls, handler, fmt, username=None, style=DEFAULT_FORMAT_STYLE):
1280 """
1281 Install the :class:`UserNameFilter` (only if needed).
1282
1283 :param fmt: The log format string to check for ``%(username)``.
1284 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
1285 :data:`DEFAULT_FORMAT_STYLE`).
1286 :param handler: The logging handler on which to install the filter.
1287 :param username: Refer to :func:`__init__()`.
1288
1289 If `fmt` is given the filter will only be installed if `fmt` uses the
1290 ``username`` field. If `fmt` is not given the filter is installed
1291 unconditionally.
1292 """
1293 if fmt:
1294 parser = FormatStringParser(style=style)
1295 if not parser.contains_field(fmt, 'username'):
1296 return
1297 handler.addFilter(cls(username))
1298
1299 def __init__(self, username=None):
1300 """
1301 Initialize a :class:`UserNameFilter` object.
1302
1303 :param username: The username to use (defaults to the
1304 result of :func:`find_username()`).
1305 """
1306 self.username = username or find_username()
1307
1308 def filter(self, record):
1309 """Set each :class:`~logging.LogRecord`'s `username` field."""
1310 # Modify the record.
1311 record.username = self.username
1312 # Don't filter the record.
1313 return 1
1314
1315
1316 class StandardErrorHandler(logging.StreamHandler):
1317
1318 """
1319 A :class:`~logging.StreamHandler` that gets the value of :data:`sys.stderr` for each log message.
1320
1321 The :class:`StandardErrorHandler` class enables `monkey patching of
1322 sys.stderr <https://github.com/xolox/python-coloredlogs/pull/31>`_. It's
1323 basically the same as the ``logging._StderrHandler`` class present in
1324 Python 3 but it will be available regardless of Python version. This
1325 handler is used by :func:`coloredlogs.install()` to improve compatibility
1326 with the Python standard library.
1327 """
1328
1329 def __init__(self, level=logging.NOTSET):
1330 """Initialize a :class:`StandardErrorHandler` object."""
1331 logging.Handler.__init__(self, level)
1332
1333 @property
1334 def stream(self):
1335 """Get the value of :data:`sys.stderr` (a file-like object)."""
1336 return sys.stderr
1337
1338
1339 class FormatStringParser(object):
1340
1341 """
1342 Shallow logging format string parser.
1343
1344 This class enables introspection and manipulation of logging format strings
1345 in the three styles supported by the :mod:`logging` module starting from
1346 Python 3.2 (``%``, ``{`` and ``$``).
1347 """
1348
1349 def __init__(self, style=DEFAULT_FORMAT_STYLE):
1350 """
1351 Initialize a :class:`FormatStringParser` object.
1352
1353 :param style: One of the characters ``%``, ``{`` or ``$`` (defaults to
1354 :data:`DEFAULT_FORMAT_STYLE`).
1355 :raises: Refer to :func:`check_style()`.
1356 """
1357 self.style = check_style(style)
1358 self.capturing_pattern = FORMAT_STYLE_PATTERNS[style]
1359 # Remove the capture group around the mapping key / field name.
1360 self.raw_pattern = self.capturing_pattern.replace(r'(\w+)', r'\w+')
1361 # After removing the inner capture group we add an outer capture group
1362 # to make the pattern suitable for simple tokenization using re.split().
1363 self.tokenize_pattern = re.compile('(%s)' % self.raw_pattern, re.VERBOSE)
1364 # Compile a regular expression for finding field names.
1365 self.name_pattern = re.compile(self.capturing_pattern, re.VERBOSE)
1366
1367 def contains_field(self, format_string, field_name):
1368 """
1369 Get the field names referenced by a format string.
1370
1371 :param format_string: The logging format string.
1372 :returns: A list of strings with field names.
1373 """
1374 return field_name in self.get_field_names(format_string)
1375
1376 def get_field_names(self, format_string):
1377 """
1378 Get the field names referenced by a format string.
1379
1380 :param format_string: The logging format string.
1381 :returns: A list of strings with field names.
1382 """
1383 return self.name_pattern.findall(format_string)
1384
1385 def get_grouped_pairs(self, format_string):
1386 """
1387 Group the results of :func:`get_pairs()` separated by whitespace.
1388
1389 :param format_string: The logging format string.
1390 :returns: A list of lists of :class:`FormatStringToken` objects.
1391 """
1392 # Step 1: Split simple tokens (without a name) into
1393 # their whitespace parts and non-whitespace parts.
1394 separated = []
1395 pattern = re.compile(r'(\s+)')
1396 for token in self.get_pairs(format_string):
1397 if token.name:
1398 separated.append(token)
1399 else:
1400 separated.extend(
1401 FormatStringToken(name=None, text=text)
1402 for text in pattern.split(token.text) if text
1403 )
1404 # Step 2: Group tokens together based on whitespace.
1405 current_group = []
1406 grouped_pairs = []
1407 for token in separated:
1408 if token.text.isspace():
1409 if current_group:
1410 grouped_pairs.append(current_group)
1411 grouped_pairs.append([token])
1412 current_group = []
1413 else:
1414 current_group.append(token)
1415 if current_group:
1416 grouped_pairs.append(current_group)
1417 return grouped_pairs
1418
1419 def get_pairs(self, format_string):
1420 """
1421 Tokenize a logging format string and extract field names from tokens.
1422
1423 :param format_string: The logging format string.
1424 :returns: A generator of :class:`FormatStringToken` objects.
1425 """
1426 for token in self.get_tokens(format_string):
1427 match = self.name_pattern.search(token)
1428 name = match.group(1) if match else None
1429 yield FormatStringToken(name=name, text=token)
1430
1431 def get_pattern(self, field_name):
1432 """
1433 Get a regular expression to match a formatting directive that references the given field name.
1434
1435 :param field_name: The name of the field to match (a string).
1436 :returns: A compiled regular expression object.
1437 """
1438 return re.compile(self.raw_pattern.replace(r'\w+', field_name), re.VERBOSE)
1439
1440 def get_tokens(self, format_string):
1441 """
1442 Tokenize a logging format string.
1443
1444 :param format_string: The logging format string.
1445 :returns: A list of strings with formatting directives separated from surrounding text.
1446 """
1447 return [t for t in self.tokenize_pattern.split(format_string) if t]
1448
1449
1450 class FormatStringToken(collections.namedtuple('FormatStringToken', 'text, name')):
1451
1452 """
1453 A named tuple for the results of :func:`FormatStringParser.get_pairs()`.
1454
1455 .. attribute:: name
1456
1457 The field name referenced in `text` (a string). If `text` doesn't
1458 contain a formatting directive this will be :data:`None`.
1459
1460 .. attribute:: text
1461
1462 The text extracted from the logging format string (a string).
1463 """
1464
1465
1466 class NameNormalizer(object):
1467
1468 """Responsible for normalizing field and level names."""
1469
1470 def __init__(self):
1471 """Initialize a :class:`NameNormalizer` object."""
1472 self.aliases = {k.lower(): v.lower() for k, v in find_level_aliases().items()}
1473
1474 def normalize_name(self, name):
1475 """
1476 Normalize a field or level name.
1477
1478 :param name: The field or level name (a string).
1479 :returns: The normalized name (a string).
1480
1481 Transforms all strings to lowercase and resolves level name aliases
1482 (refer to :func:`find_level_aliases()`) to their canonical name:
1483
1484 >>> from coloredlogs import NameNormalizer
1485 >>> from humanfriendly import format_table
1486 >>> nn = NameNormalizer()
1487 >>> sample_names = ['DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL', 'CRITICAL']
1488 >>> print(format_table([(n, nn.normalize_name(n)) for n in sample_names]))
1489 -----------------------
1490 | DEBUG | debug |
1491 | INFO | info |
1492 | WARN | warning |
1493 | WARNING | warning |
1494 | ERROR | error |
1495 | FATAL | critical |
1496 | CRITICAL | critical |
1497 -----------------------
1498 """
1499 name = name.lower()
1500 if name in self.aliases:
1501 name = self.aliases[name]
1502 return name
1503
1504 def normalize_keys(self, value):
1505 """
1506 Normalize the keys of a dictionary using :func:`normalize_name()`.
1507
1508 :param value: The dictionary to normalize.
1509 :returns: A dictionary with normalized keys.
1510 """
1511 return {self.normalize_name(k): v for k, v in value.items()}
1512
1513 def get(self, normalized_dict, name):
1514 """
1515 Get a value from a dictionary after normalizing the key.
1516
1517 :param normalized_dict: A dictionary produced by :func:`normalize_keys()`.
1518 :param name: A key to normalize and get from the dictionary.
1519 :returns: The value of the normalized key (if any).
1520 """
1521 return normalized_dict.get(self.normalize_name(name))