• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Tools for configuring Python logging."""
15
16import logging
17from pathlib import Path
18from typing import NamedTuple, Optional, Union, Iterator
19
20import pw_cli.color
21import pw_cli.env
22import pw_cli.plugins
23
24# Log level used for captured output of a subprocess run through pw.
25LOGLEVEL_STDOUT = 21
26
27# Log level indicating a irrecoverable failure.
28LOGLEVEL_FATAL = 70
29
30
31class _LogLevel(NamedTuple):
32    level: int
33    color: str
34    ascii: str
35    emoji: str
36
37
38# Shorten all the log levels to 3 characters for column-aligned logs.
39# Color the logs using ANSI codes.
40_LOG_LEVELS = (
41    _LogLevel(LOGLEVEL_FATAL,   'bold_red',     'FTL', '☠️ '),
42    _LogLevel(logging.CRITICAL, 'bold_magenta', 'CRT', '‼️ '),
43    _LogLevel(logging.ERROR,    'red',          'ERR', '❌'),
44    _LogLevel(logging.WARNING,  'yellow',       'WRN', '⚠️ '),
45    _LogLevel(logging.INFO,     'magenta',      'INF', 'ℹ️ '),
46    _LogLevel(LOGLEVEL_STDOUT,  'cyan',         'OUT', '��'),
47    _LogLevel(logging.DEBUG,    'blue',         'DBG', '��'),
48)  # yapf: disable
49
50_LOG = logging.getLogger(__name__)
51_STDERR_HANDLER = logging.StreamHandler()
52
53
54def c_to_py_log_level(c_level: int) -> int:
55    """Converts pw_log C log-level macros to Python logging levels."""
56    return c_level * 10
57
58
59def main() -> None:
60    """Shows how logs look at various levels."""
61
62    # Force the log level to make sure all logs are shown.
63    _LOG.setLevel(logging.DEBUG)
64
65    # Log one message for every log level.
66    _LOG.log(LOGLEVEL_FATAL, 'An irrecoverable error has occurred!')
67    _LOG.critical('Something important has happened!')
68    _LOG.error('There was an error on our last operation')
69    _LOG.warning('Looks like something is amiss; consider investigating')
70    _LOG.info('The operation went as expected')
71    _LOG.log(LOGLEVEL_STDOUT, 'Standard output of subprocess')
72    _LOG.debug('Adding 1 to i')
73
74
75def _setup_handler(handler: logging.Handler, formatter: logging.Formatter,
76                   level: Union[str, int], logger: logging.Logger) -> None:
77    handler.setLevel(level)
78    handler.setFormatter(formatter)
79    logger.addHandler(handler)
80
81
82def install(level: Union[str, int] = logging.INFO,
83            use_color: bool = None,
84            hide_timestamp: bool = False,
85            log_file: Union[str, Path] = None,
86            logger: Optional[logging.Logger] = None) -> None:
87    """Configures the system logger for the default pw command log format.
88
89    If you have Python loggers separate from the root logger you can use
90    `pw_cli.log.install` to get the Pigweed log formatting there too. For
91    example: ::
92
93        import logging
94
95        import pw_cli.log
96
97        pw_cli.log.install(
98            level=logging.INFO,
99            use_color=True,
100            hide_timestamp=False,
101            log_file=(Path.home() / 'logs.txt'),
102            logger=logging.getLogger(__package__),
103        )
104
105    Args:
106      level: The logging level to apply. Default: `logging.INFO`.
107      use_color: When `True` include ANSI escape sequences to colorize log
108          messages.
109      hide_timestamp: When `True` omit timestamps from the log formatting.
110      log_file: File to save logs into.
111      logger: Python Logger instance to install Pigweed formatting into.
112          Defaults to the Python root logger: `logging.getLogger()`.
113
114    """
115    if not logger:
116        logger = logging.getLogger()
117
118    colors = pw_cli.color.colors(use_color)
119
120    env = pw_cli.env.pigweed_environment()
121    if env.PW_SUBPROCESS or hide_timestamp:
122        # If the logger is being run in the context of a pw subprocess, the
123        # time and date are omitted (since pw_cli.process will provide them).
124        timestamp_fmt = ''
125    else:
126        # This applies a gray background to the time to make the log lines
127        # distinct from other input, in a way that's easier to see than plain
128        # colored text.
129        timestamp_fmt = colors.black_on_white('%(asctime)s') + ' '
130
131    formatter = logging.Formatter(timestamp_fmt + '%(levelname)s %(message)s',
132                                  '%Y%m%d %H:%M:%S')
133
134    # Set the log level on the root logger to NOTSET, so that all logs
135    # propagated from child loggers are handled.
136    logging.getLogger().setLevel(logging.NOTSET)
137
138    # Always set up the stderr handler, even if it isn't used.
139    _setup_handler(_STDERR_HANDLER, formatter, level, logger)
140
141    if log_file:
142        # Set utf-8 encoding for the log file. Encoding errors may come up on
143        # Windows if the default system encoding is set to cp1250.
144        _setup_handler(logging.FileHandler(log_file, encoding='utf-8'),
145                       formatter, level, logger)
146        # Since we're using a file, filter logs out of the stderr handler.
147        _STDERR_HANDLER.setLevel(logging.CRITICAL + 1)
148
149    if env.PW_EMOJI:
150        name_attr = 'emoji'
151        colorize = lambda ll: str
152    else:
153        name_attr = 'ascii'
154        colorize = lambda ll: getattr(colors, ll.color)
155
156    for log_level in _LOG_LEVELS:
157        name = getattr(log_level, name_attr)
158        logging.addLevelName(log_level.level, colorize(log_level)(name))
159
160
161def all_loggers() -> Iterator[logging.Logger]:
162    """Iterates over all loggers known to Python logging."""
163    manager = logging.getLogger().manager  # type: ignore[attr-defined]
164
165    for logger_name in manager.loggerDict:  # pylint: disable=no-member
166        yield logging.getLogger(logger_name)
167
168
169def set_all_loggers_minimum_level(level: int) -> None:
170    """Increases the log level to the specified value for all known loggers."""
171    for logger in all_loggers():
172        if logger.isEnabledFor(level - 1):
173            logger.setLevel(level)
174
175
176if __name__ == '__main__':
177    install()
178    main()
179