aboutsummaryrefslogtreecommitdiff
path: root/pw_cli/py/pw_cli/log.py
blob: efe4f4034280673ba812d29df1c36f2a2fc5669c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# 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:
        _setup_handler(logging.FileHandler(log_file), 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()