xref: /openbmc/openbmc/poky/bitbake/lib/bb/msg.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
1"""
2BitBake 'msg' implementation
3
4Message handling infrastructure for bitbake
5
6"""
7
8# Copyright (C) 2006        Richard Purdie
9#
10# SPDX-License-Identifier: GPL-2.0-only
11#
12
13import sys
14import copy
15import logging
16import logging.config
17import os
18from itertools import groupby
19import bb
20import bb.event
21
22class BBLogFormatter(logging.Formatter):
23    """Formatter which ensures that our 'plain' messages (logging.INFO + 1) are used as is"""
24
25    DEBUG3 = logging.DEBUG - 2
26    DEBUG2 = logging.DEBUG - 1
27    DEBUG = logging.DEBUG
28    VERBOSE = logging.INFO - 1
29    NOTE = logging.INFO
30    PLAIN = logging.INFO + 1
31    VERBNOTE = logging.INFO + 2
32    ERROR = logging.ERROR
33    ERRORONCE = logging.ERROR - 1
34    WARNING = logging.WARNING
35    WARNONCE = logging.WARNING - 1
36    CRITICAL = logging.CRITICAL
37
38    levelnames = {
39        DEBUG3   : 'DEBUG',
40        DEBUG2   : 'DEBUG',
41        DEBUG   : 'DEBUG',
42        VERBOSE: 'NOTE',
43        NOTE    : 'NOTE',
44        PLAIN  : '',
45        VERBNOTE: 'NOTE',
46        WARNING : 'WARNING',
47        WARNONCE : 'WARNING',
48        ERROR   : 'ERROR',
49        ERRORONCE   : 'ERROR',
50        CRITICAL: 'ERROR',
51    }
52
53    color_enabled = False
54    BASECOLOR, BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(29,38))
55
56    COLORS = {
57        DEBUG3  : CYAN,
58        DEBUG2  : CYAN,
59        DEBUG   : CYAN,
60        VERBOSE : BASECOLOR,
61        NOTE    : BASECOLOR,
62        PLAIN   : BASECOLOR,
63        VERBNOTE: BASECOLOR,
64        WARNING : YELLOW,
65        WARNONCE : YELLOW,
66        ERROR   : RED,
67        ERRORONCE : RED,
68        CRITICAL: RED,
69    }
70
71    BLD = '\033[1;%dm'
72    STD = '\033[%dm'
73    RST = '\033[0m'
74
75    def getLevelName(self, levelno):
76        try:
77            return self.levelnames[levelno]
78        except KeyError:
79            self.levelnames[levelno] = value = 'Level %d' % levelno
80            return value
81
82    def format(self, record):
83        record.levelname = self.getLevelName(record.levelno)
84        if record.levelno == self.PLAIN:
85            msg = record.getMessage()
86        else:
87            if self.color_enabled:
88                record = self.colorize(record)
89            msg = logging.Formatter.format(self, record)
90        if hasattr(record, 'bb_exc_formatted'):
91            msg += '\n' + ''.join(record.bb_exc_formatted)
92        return msg
93
94    def colorize(self, record):
95        color = self.COLORS[record.levelno]
96        if self.color_enabled and color is not None:
97            record = copy.copy(record)
98            record.levelname = "".join([self.BLD % color, record.levelname, self.RST])
99            record.msg = "".join([self.STD % color, record.msg, self.RST])
100        return record
101
102    def enable_color(self):
103        self.color_enabled = True
104
105    def __repr__(self):
106        return "%s fmt='%s' color=%s" % (self.__class__.__name__, self._fmt, "True" if self.color_enabled else "False")
107
108class BBLogFilter(object):
109    def __init__(self, handler, level, debug_domains):
110        self.stdlevel = level
111        self.debug_domains = debug_domains
112        loglevel = level
113        for domain in debug_domains:
114            if debug_domains[domain] < loglevel:
115                loglevel = debug_domains[domain]
116        handler.setLevel(loglevel)
117        handler.addFilter(self)
118
119    def filter(self, record):
120        if record.levelno >= self.stdlevel:
121            return True
122        if record.name in self.debug_domains and record.levelno >= self.debug_domains[record.name]:
123            return True
124        return False
125
126class LogFilterShowOnce(logging.Filter):
127    def __init__(self):
128        self.seen_warnings = set()
129        self.seen_errors = set()
130
131    def filter(self, record):
132        if record.levelno == bb.msg.BBLogFormatter.WARNONCE:
133            if record.msg in self.seen_warnings:
134                return False
135            self.seen_warnings.add(record.msg)
136        if record.levelno == bb.msg.BBLogFormatter.ERRORONCE:
137            if record.msg in self.seen_errors:
138                return False
139            self.seen_errors.add(record.msg)
140        return True
141
142class LogFilterGEQLevel(logging.Filter):
143    def __init__(self, level):
144        self.strlevel = str(level)
145        self.level = stringToLevel(level)
146
147    def __repr__(self):
148        return "%s level >= %s (%d)" % (self.__class__.__name__, self.strlevel, self.level)
149
150    def filter(self, record):
151        return (record.levelno >= self.level)
152
153class LogFilterLTLevel(logging.Filter):
154    def __init__(self, level):
155        self.strlevel = str(level)
156        self.level = stringToLevel(level)
157
158    def __repr__(self):
159        return "%s level < %s (%d)" % (self.__class__.__name__, self.strlevel, self.level)
160
161    def filter(self, record):
162        return (record.levelno < self.level)
163
164# Message control functions
165#
166
167loggerDefaultLogLevel = BBLogFormatter.NOTE
168loggerDefaultDomains = {}
169
170def init_msgconfig(verbose, debug, debug_domains=None):
171    """
172    Set default verbosity and debug levels config the logger
173    """
174    if debug:
175        bb.msg.loggerDefaultLogLevel = BBLogFormatter.DEBUG - debug + 1
176    elif verbose:
177        bb.msg.loggerDefaultLogLevel = BBLogFormatter.VERBOSE
178    else:
179        bb.msg.loggerDefaultLogLevel = BBLogFormatter.NOTE
180
181    bb.msg.loggerDefaultDomains = {}
182    if debug_domains:
183        for (domainarg, iterator) in groupby(debug_domains):
184            dlevel = len(tuple(iterator))
185            bb.msg.loggerDefaultDomains["BitBake.%s" % domainarg] = logging.DEBUG - dlevel + 1
186
187def constructLogOptions():
188    return loggerDefaultLogLevel, loggerDefaultDomains
189
190def addDefaultlogFilter(handler, cls = BBLogFilter, forcelevel=None):
191    level, debug_domains = constructLogOptions()
192
193    if forcelevel is not None:
194        level = forcelevel
195
196    cls(handler, level, debug_domains)
197
198def stringToLevel(level):
199    try:
200        return int(level)
201    except ValueError:
202        pass
203
204    try:
205        return getattr(logging, level)
206    except AttributeError:
207        pass
208
209    return getattr(BBLogFormatter, level)
210
211#
212# Message handling functions
213#
214
215def fatal(msgdomain, msg):
216    if msgdomain:
217        logger = logging.getLogger("BitBake.%s" % msgdomain)
218    else:
219        logger = logging.getLogger("BitBake")
220    logger.critical(msg)
221    sys.exit(1)
222
223def logger_create(name, output=sys.stderr, level=logging.INFO, preserve_handlers=False, color='auto'):
224    """Standalone logger creation function"""
225    logger = logging.getLogger(name)
226    console = logging.StreamHandler(output)
227    console.addFilter(bb.msg.LogFilterShowOnce())
228    format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
229    if color == 'always' or (color == 'auto' and output.isatty() and os.environ.get('NO_COLOR', '') == ''):
230        format.enable_color()
231    console.setFormatter(format)
232    if preserve_handlers:
233        logger.addHandler(console)
234    else:
235        logger.handlers = [console]
236    logger.setLevel(level)
237    return logger
238
239def has_console_handler(logger):
240    for handler in logger.handlers:
241        if isinstance(handler, logging.StreamHandler):
242            if handler.stream in [sys.stderr, sys.stdout]:
243                return True
244    return False
245
246def mergeLoggingConfig(logconfig, userconfig):
247    logconfig = copy.deepcopy(logconfig)
248    userconfig = copy.deepcopy(userconfig)
249
250    # Merge config with the default config
251    if userconfig.get('version') != logconfig['version']:
252        raise BaseException("Bad user configuration version. Expected %r, got %r" % (logconfig['version'], userconfig.get('version')))
253
254    # Set some defaults to make merging easier
255    userconfig.setdefault("loggers", {})
256
257    # If a handler, formatter, or filter is defined in the user
258    # config, it will replace an existing one in the default config
259    for k in ("handlers", "formatters", "filters"):
260        logconfig.setdefault(k, {}).update(userconfig.get(k, {}))
261
262    seen_loggers = set()
263    for name, l in logconfig["loggers"].items():
264        # If the merge option is set, merge the handlers and
265        # filters. Otherwise, if it is False, this logger won't get
266        # add to the set of seen loggers and will replace the
267        # existing one
268        if l.get('bitbake_merge', True):
269            ulogger = userconfig["loggers"].setdefault(name, {})
270            ulogger.setdefault("handlers", [])
271            ulogger.setdefault("filters", [])
272
273            # Merge lists
274            l.setdefault("handlers", []).extend(ulogger["handlers"])
275            l.setdefault("filters", []).extend(ulogger["filters"])
276
277            # Replace other properties if present
278            if "level" in ulogger:
279                l["level"] = ulogger["level"]
280
281            if "propagate" in ulogger:
282                l["propagate"] = ulogger["propagate"]
283
284            seen_loggers.add(name)
285
286    # Add all loggers present in the user config, but not any that
287    # have already been processed
288    for name in set(userconfig["loggers"].keys()) - seen_loggers:
289        logconfig["loggers"][name] = userconfig["loggers"][name]
290
291    return logconfig
292
293def setLoggingConfig(defaultconfig, userconfigfile=None):
294    logconfig = copy.deepcopy(defaultconfig)
295
296    if userconfigfile:
297        with open(os.path.normpath(userconfigfile), 'r') as f:
298            if userconfigfile.endswith('.yml') or userconfigfile.endswith('.yaml'):
299                import yaml
300                userconfig = yaml.safe_load(f)
301            elif userconfigfile.endswith('.json') or userconfigfile.endswith('.cfg'):
302                import json
303                userconfig = json.load(f)
304            else:
305                raise BaseException("Unrecognized file format: %s" % userconfigfile)
306
307            if userconfig.get('bitbake_merge', True):
308                logconfig = mergeLoggingConfig(logconfig, userconfig)
309            else:
310                # Replace the entire default config
311                logconfig = userconfig
312
313    # Convert all level parameters to integers in case users want to use the
314    # bitbake defined level names
315    for name, h in logconfig["handlers"].items():
316        if "level" in h:
317            h["level"] = bb.msg.stringToLevel(h["level"])
318
319        # Every handler needs its own instance of the once filter.
320        once_filter_name = name + ".showonceFilter"
321        logconfig.setdefault("filters", {})[once_filter_name] = {
322            "()": "bb.msg.LogFilterShowOnce",
323        }
324        h.setdefault("filters", []).append(once_filter_name)
325
326    for l in logconfig["loggers"].values():
327        if "level" in l:
328            l["level"] = bb.msg.stringToLevel(l["level"])
329
330    conf = logging.config.dictConfigClass(logconfig)
331    conf.configure()
332
333    # The user may have specified logging domains they want at a higher debug
334    # level than the standard.
335    for name, l in logconfig["loggers"].items():
336        if not name.startswith("BitBake."):
337            continue
338
339        if not "level" in l:
340            continue
341
342        curlevel = bb.msg.loggerDefaultDomains.get(name)
343        # Note: level parameter should already be a int because of conversion
344        # above
345        newlevel = int(l["level"])
346        if curlevel is None or newlevel < curlevel:
347            bb.msg.loggerDefaultDomains[name] = newlevel
348
349        # TODO: I don't think that setting the global log level should be necessary
350        #if newlevel < bb.msg.loggerDefaultLogLevel:
351        #    bb.msg.loggerDefaultLogLevel = newlevel
352
353    return conf
354