blob: 3ef755470f58a71d0bd9c16bad88d71cb3679973 [file] [log] [blame]
# 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()