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, 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 28class _LogLevel(NamedTuple): 29 level: int 30 color: str 31 ascii: str 32 emoji: str 33 34 35# Shorten all the log levels to 3 characters for column-aligned logs. 36# Color the logs using ANSI codes. 37_LOG_LEVELS = ( 38 _LogLevel(logging.CRITICAL, 'bold_red', 'CRT', '☠️ '), 39 _LogLevel(logging.ERROR, 'red', 'ERR', '❌'), 40 _LogLevel(logging.WARNING, 'yellow', 'WRN', '⚠️ '), 41 _LogLevel(logging.INFO, 'magenta', 'INF', 'ℹ️ '), 42 _LogLevel(LOGLEVEL_STDOUT, 'cyan', 'OUT', ''), 43 _LogLevel(logging.DEBUG, 'blue', 'DBG', ''), 44) # yapf: disable 45 46_LOG = logging.getLogger(__name__) 47_STDERR_HANDLER = logging.StreamHandler() 48 49 50def main() -> None: 51 """Shows how logs look at various levels.""" 52 53 # Force the log level to make sure all logs are shown. 54 _LOG.setLevel(logging.DEBUG) 55 56 # Log one message for every log level. 57 _LOG.critical('Something terrible has happened!') 58 _LOG.error('There was an error on our last operation') 59 _LOG.warning('Looks like something is amiss; consider investigating') 60 _LOG.info('The operation went as expected') 61 _LOG.log(LOGLEVEL_STDOUT, 'Standard output of subprocess') 62 _LOG.debug('Adding 1 to i') 63 64 65def _setup_handler(handler: logging.Handler, formatter: logging.Formatter, 66 level: int) -> None: 67 handler.setLevel(level) 68 handler.setFormatter(formatter) 69 logging.getLogger().addHandler(handler) 70 71 72def install(level: int = logging.INFO, 73 use_color: bool = None, 74 hide_timestamp: bool = False, 75 log_file: Union[str, Path] = None) -> None: 76 """Configures the system logger for the default pw command log format.""" 77 78 colors = pw_cli.color.colors(use_color) 79 80 env = pw_cli.env.pigweed_environment() 81 if env.PW_SUBPROCESS or hide_timestamp: 82 # If the logger is being run in the context of a pw subprocess, the 83 # time and date are omitted (since pw_cli.process will provide them). 84 timestamp_fmt = '' 85 else: 86 # This applies a gray background to the time to make the log lines 87 # distinct from other input, in a way that's easier to see than plain 88 # colored text. 89 timestamp_fmt = colors.black_on_white('%(asctime)s') + ' ' 90 91 formatter = logging.Formatter(timestamp_fmt + '%(levelname)s %(message)s', 92 '%Y%m%d %H:%M:%S') 93 94 # Set the log level on the root logger to 1, so logs that all logs 95 # propagated from child loggers are handled. 96 logging.getLogger().setLevel(1) 97 98 # Always set up the stderr handler, even if it isn't used. 99 _setup_handler(_STDERR_HANDLER, formatter, level) 100 101 if log_file: 102 _setup_handler(logging.FileHandler(log_file), formatter, level) 103 # Since we're using a file, filter logs out of the stderr handler. 104 _STDERR_HANDLER.setLevel(logging.CRITICAL + 1) 105 106 if env.PW_EMOJI: 107 name_attr = 'emoji' 108 colorize = lambda ll: str 109 else: 110 name_attr = 'ascii' 111 colorize = lambda ll: getattr(colors, ll.color) 112 113 for log_level in _LOG_LEVELS: 114 name = getattr(log_level, name_attr) 115 logging.addLevelName(log_level.level, colorize(log_level)(name)) 116 117 118def all_loggers() -> Iterator[logging.Logger]: 119 """Iterates over all loggers known to Python logging.""" 120 manager = logging.getLogger().manager # type: ignore[attr-defined] 121 122 for logger_name in manager.loggerDict: # pylint: disable=no-member 123 yield logging.getLogger(logger_name) 124 125 126def set_all_loggers_minimum_level(level: int) -> None: 127 """Increases the log level to the specified value for all known loggers.""" 128 for logger in all_loggers(): 129 if logger.isEnabledFor(level - 1): 130 logger.setLevel(level) 131 132 133if __name__ == '__main__': 134 install() 135 main() 136