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