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