| # Copyright 2020 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Tools for configuring Python logging.""" |
| |
| import logging |
| from pathlib import Path |
| from typing import NamedTuple, Optional, Union, Iterator |
| |
| import pw_cli.color |
| import pw_cli.env |
| import pw_cli.plugins |
| |
| # Log level used for captured output of a subprocess run through pw. |
| LOGLEVEL_STDOUT = 21 |
| |
| # Log level indicating a irrecoverable failure. |
| LOGLEVEL_FATAL = 70 |
| |
| |
| class _LogLevel(NamedTuple): |
| level: int |
| color: str |
| ascii: str |
| emoji: str |
| |
| |
| # Shorten all the log levels to 3 characters for column-aligned logs. |
| # Color the logs using ANSI codes. |
| _LOG_LEVELS = ( |
| _LogLevel(LOGLEVEL_FATAL, 'bold_red', 'FTL', '☠️ '), |
| _LogLevel(logging.CRITICAL, 'bold_magenta', 'CRT', '‼️ '), |
| _LogLevel(logging.ERROR, 'red', 'ERR', '❌'), |
| _LogLevel(logging.WARNING, 'yellow', 'WRN', '⚠️ '), |
| _LogLevel(logging.INFO, 'magenta', 'INF', 'ℹ️ '), |
| _LogLevel(LOGLEVEL_STDOUT, 'cyan', 'OUT', '💬'), |
| _LogLevel(logging.DEBUG, 'blue', 'DBG', '👾'), |
| ) # yapf: disable |
| |
| _LOG = logging.getLogger(__name__) |
| _STDERR_HANDLER = logging.StreamHandler() |
| |
| |
| def c_to_py_log_level(c_level: int) -> int: |
| """Converts pw_log C log-level macros to Python logging levels.""" |
| return c_level * 10 |
| |
| |
| def main() -> None: |
| """Shows how logs look at various levels.""" |
| |
| # Force the log level to make sure all logs are shown. |
| _LOG.setLevel(logging.DEBUG) |
| |
| # Log one message for every log level. |
| _LOG.log(LOGLEVEL_FATAL, 'An irrecoverable error has occurred!') |
| _LOG.critical('Something important has happened!') |
| _LOG.error('There was an error on our last operation') |
| _LOG.warning('Looks like something is amiss; consider investigating') |
| _LOG.info('The operation went as expected') |
| _LOG.log(LOGLEVEL_STDOUT, 'Standard output of subprocess') |
| _LOG.debug('Adding 1 to i') |
| |
| |
| def _setup_handler(handler: logging.Handler, formatter: logging.Formatter, |
| level: Union[str, int], logger: logging.Logger) -> None: |
| handler.setLevel(level) |
| handler.setFormatter(formatter) |
| logger.addHandler(handler) |
| |
| |
| def install(level: Union[str, int] = logging.INFO, |
| use_color: bool = None, |
| hide_timestamp: bool = False, |
| log_file: Union[str, Path] = None, |
| logger: Optional[logging.Logger] = None) -> None: |
| """Configures the system logger for the default pw command log format. |
| |
| If you have Python loggers separate from the root logger you can use |
| `pw_cli.log.install` to get the Pigweed log formatting there too. For |
| example: :: |
| |
| import logging |
| |
| import pw_cli.log |
| |
| pw_cli.log.install( |
| level=logging.INFO, |
| use_color=True, |
| hide_timestamp=False, |
| log_file=(Path.home() / 'logs.txt'), |
| logger=logging.getLogger(__package__), |
| ) |
| |
| Args: |
| level: The logging level to apply. Default: `logging.INFO`. |
| use_color: When `True` include ANSI escape sequences to colorize log |
| messages. |
| hide_timestamp: When `True` omit timestamps from the log formatting. |
| log_file: File to save logs into. |
| logger: Python Logger instance to install Pigweed formatting into. |
| Defaults to the Python root logger: `logging.getLogger()`. |
| |
| """ |
| if not logger: |
| logger = logging.getLogger() |
| |
| colors = pw_cli.color.colors(use_color) |
| |
| env = pw_cli.env.pigweed_environment() |
| if env.PW_SUBPROCESS or hide_timestamp: |
| # If the logger is being run in the context of a pw subprocess, the |
| # time and date are omitted (since pw_cli.process will provide them). |
| timestamp_fmt = '' |
| else: |
| # This applies a gray background to the time to make the log lines |
| # distinct from other input, in a way that's easier to see than plain |
| # colored text. |
| timestamp_fmt = colors.black_on_white('%(asctime)s') + ' ' |
| |
| formatter = logging.Formatter(timestamp_fmt + '%(levelname)s %(message)s', |
| '%Y%m%d %H:%M:%S') |
| |
| # Set the log level on the root logger to NOTSET, so that all logs |
| # propagated from child loggers are handled. |
| logging.getLogger().setLevel(logging.NOTSET) |
| |
| # Always set up the stderr handler, even if it isn't used. |
| _setup_handler(_STDERR_HANDLER, formatter, level, logger) |
| |
| if log_file: |
| # Set utf-8 encoding for the log file. Encoding errors may come up on |
| # Windows if the default system encoding is set to cp1250. |
| _setup_handler(logging.FileHandler(log_file, encoding='utf-8'), |
| formatter, level, logger) |
| # Since we're using a file, filter logs out of the stderr handler. |
| _STDERR_HANDLER.setLevel(logging.CRITICAL + 1) |
| |
| if env.PW_EMOJI: |
| name_attr = 'emoji' |
| colorize = lambda ll: str |
| else: |
| name_attr = 'ascii' |
| colorize = lambda ll: getattr(colors, ll.color) |
| |
| for log_level in _LOG_LEVELS: |
| name = getattr(log_level, name_attr) |
| logging.addLevelName(log_level.level, colorize(log_level)(name)) |
| |
| |
| def all_loggers() -> Iterator[logging.Logger]: |
| """Iterates over all loggers known to Python logging.""" |
| manager = logging.getLogger().manager # type: ignore[attr-defined] |
| |
| for logger_name in manager.loggerDict: # pylint: disable=no-member |
| yield logging.getLogger(logger_name) |
| |
| |
| def set_all_loggers_minimum_level(level: int) -> None: |
| """Increases the log level to the specified value for all known loggers.""" |
| for logger in all_loggers(): |
| if logger.isEnabledFor(level - 1): |
| logger.setLevel(level) |
| |
| |
| if __name__ == '__main__': |
| install() |
| main() |