xref: /openbmc/openbmc/poky/bitbake/lib/bb/msg.py (revision 03514f19)
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        elif hasattr(record, 'bb_exc_info'):
93            etype, value, tb = record.bb_exc_info
94            formatted = bb.exceptions.format_exception(etype, value, tb, limit=5)
95            msg += '\n' + ''.join(formatted)
96        return msg
97
98    def colorize(self, record):
99        color = self.COLORS[record.levelno]
100        if self.color_enabled and color is not None:
101            record = copy.copy(record)
102            record.levelname = "".join([self.BLD % color, record.levelname, self.RST])
103            record.msg = "".join([self.STD % color, record.msg, self.RST])
104        return record
105
106    def enable_color(self):
107        self.color_enabled = True
108
109    def __repr__(self):
110        return "%s fmt='%s' color=%s" % (self.__class__.__name__, self._fmt, "True" if self.color_enabled else "False")
111
112class BBLogFilter(object):
113    def __init__(self, handler, level, debug_domains):
114        self.stdlevel = level
115        self.debug_domains = debug_domains
116        loglevel = level
117        for domain in debug_domains:
118            if debug_domains[domain] < loglevel:
119                loglevel = debug_domains[domain]
120        handler.setLevel(loglevel)
121        handler.addFilter(self)
122
123    def filter(self, record):
124        if record.levelno >= self.stdlevel:
125            return True
126        if record.name in self.debug_domains and record.levelno >= self.debug_domains[record.name]:
127            return True
128        return False
129
130class LogFilterShowOnce(logging.Filter):
131    def __init__(self):
132        self.seen_warnings = set()
133        self.seen_errors = set()
134
135    def filter(self, record):
136        if record.levelno == bb.msg.BBLogFormatter.WARNONCE:
137            if record.msg in self.seen_warnings:
138                return False
139            self.seen_warnings.add(record.msg)
140        if record.levelno == bb.msg.BBLogFormatter.ERRORONCE:
141            if record.msg in self.seen_errors:
142                return False
143            self.seen_errors.add(record.msg)
144        return True
145
146class LogFilterGEQLevel(logging.Filter):
147    def __init__(self, level):
148        self.strlevel = str(level)
149        self.level = stringToLevel(level)
150
151    def __repr__(self):
152        return "%s level >= %s (%d)" % (self.__class__.__name__, self.strlevel, self.level)
153
154    def filter(self, record):
155        return (record.levelno >= self.level)
156
157class LogFilterLTLevel(logging.Filter):
158    def __init__(self, level):
159        self.strlevel = str(level)
160        self.level = stringToLevel(level)
161
162    def __repr__(self):
163        return "%s level < %s (%d)" % (self.__class__.__name__, self.strlevel, self.level)
164
165    def filter(self, record):
166        return (record.levelno < self.level)
167
168# Message control functions
169#
170
171loggerDefaultLogLevel = BBLogFormatter.NOTE
172loggerDefaultDomains = {}
173
174def init_msgconfig(verbose, debug, debug_domains=None):
175    """
176    Set default verbosity and debug levels config the logger
177    """
178    if debug:
179        bb.msg.loggerDefaultLogLevel = BBLogFormatter.DEBUG - debug + 1
180    elif verbose:
181        bb.msg.loggerDefaultLogLevel = BBLogFormatter.VERBOSE
182    else:
183        bb.msg.loggerDefaultLogLevel = BBLogFormatter.NOTE
184
185    bb.msg.loggerDefaultDomains = {}
186    if debug_domains:
187        for (domainarg, iterator) in groupby(debug_domains):
188            dlevel = len(tuple(iterator))
189            bb.msg.loggerDefaultDomains["BitBake.%s" % domainarg] = logging.DEBUG - dlevel + 1
190
191def constructLogOptions():
192    return loggerDefaultLogLevel, loggerDefaultDomains
193
194def addDefaultlogFilter(handler, cls = BBLogFilter, forcelevel=None):
195    level, debug_domains = constructLogOptions()
196
197    if forcelevel is not None:
198        level = forcelevel
199
200    cls(handler, level, debug_domains)
201
202def stringToLevel(level):
203    try:
204        return int(level)
205    except ValueError:
206        pass
207
208    try:
209        return getattr(logging, level)
210    except AttributeError:
211        pass
212
213    return getattr(BBLogFormatter, level)
214
215#
216# Message handling functions
217#
218
219def fatal(msgdomain, msg):
220    if msgdomain:
221        logger = logging.getLogger("BitBake.%s" % msgdomain)
222    else:
223        logger = logging.getLogger("BitBake")
224    logger.critical(msg)
225    sys.exit(1)
226
227def logger_create(name, output=sys.stderr, level=logging.INFO, preserve_handlers=False, color='auto'):
228    """Standalone logger creation function"""
229    logger = logging.getLogger(name)
230    console = logging.StreamHandler(output)
231    console.addFilter(bb.msg.LogFilterShowOnce())
232    format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
233    if color == 'always' or (color == 'auto' and output.isatty() and os.environ.get('NO_COLOR', '') == ''):
234        format.enable_color()
235    console.setFormatter(format)
236    if preserve_handlers:
237        logger.addHandler(console)
238    else:
239        logger.handlers = [console]
240    logger.setLevel(level)
241    return logger
242
243def has_console_handler(logger):
244    for handler in logger.handlers:
245        if isinstance(handler, logging.StreamHandler):
246            if handler.stream in [sys.stderr, sys.stdout]:
247                return True
248    return False
249
250def mergeLoggingConfig(logconfig, userconfig):
251    logconfig = copy.deepcopy(logconfig)
252    userconfig = copy.deepcopy(userconfig)
253
254    # Merge config with the default config
255    if userconfig.get('version') != logconfig['version']:
256        raise BaseException("Bad user configuration version. Expected %r, got %r" % (logconfig['version'], userconfig.get('version')))
257
258    # Set some defaults to make merging easier
259    userconfig.setdefault("loggers", {})
260
261    # If a handler, formatter, or filter is defined in the user
262    # config, it will replace an existing one in the default config
263    for k in ("handlers", "formatters", "filters"):
264        logconfig.setdefault(k, {}).update(userconfig.get(k, {}))
265
266    seen_loggers = set()
267    for name, l in logconfig["loggers"].items():
268        # If the merge option is set, merge the handlers and
269        # filters. Otherwise, if it is False, this logger won't get
270        # add to the set of seen loggers and will replace the
271        # existing one
272        if l.get('bitbake_merge', True):
273            ulogger = userconfig["loggers"].setdefault(name, {})
274            ulogger.setdefault("handlers", [])
275            ulogger.setdefault("filters", [])
276
277            # Merge lists
278            l.setdefault("handlers", []).extend(ulogger["handlers"])
279            l.setdefault("filters", []).extend(ulogger["filters"])
280
281            # Replace other properties if present
282            if "level" in ulogger:
283                l["level"] = ulogger["level"]
284
285            if "propagate" in ulogger:
286                l["propagate"] = ulogger["propagate"]
287
288            seen_loggers.add(name)
289
290    # Add all loggers present in the user config, but not any that
291    # have already been processed
292    for name in set(userconfig["loggers"].keys()) - seen_loggers:
293        logconfig["loggers"][name] = userconfig["loggers"][name]
294
295    return logconfig
296
297def setLoggingConfig(defaultconfig, userconfigfile=None):
298    logconfig = copy.deepcopy(defaultconfig)
299
300    if userconfigfile:
301        with open(os.path.normpath(userconfigfile), 'r') as f:
302            if userconfigfile.endswith('.yml') or userconfigfile.endswith('.yaml'):
303                import yaml
304                userconfig = yaml.safe_load(f)
305            elif userconfigfile.endswith('.json') or userconfigfile.endswith('.cfg'):
306                import json
307                userconfig = json.load(f)
308            else:
309                raise BaseException("Unrecognized file format: %s" % userconfigfile)
310
311            if userconfig.get('bitbake_merge', True):
312                logconfig = mergeLoggingConfig(logconfig, userconfig)
313            else:
314                # Replace the entire default config
315                logconfig = userconfig
316
317    # Convert all level parameters to integers in case users want to use the
318    # bitbake defined level names
319    for name, h in logconfig["handlers"].items():
320        if "level" in h:
321            h["level"] = bb.msg.stringToLevel(h["level"])
322
323        # Every handler needs its own instance of the once filter.
324        once_filter_name = name + ".showonceFilter"
325        logconfig.setdefault("filters", {})[once_filter_name] = {
326            "()": "bb.msg.LogFilterShowOnce",
327        }
328        h.setdefault("filters", []).append(once_filter_name)
329
330    for l in logconfig["loggers"].values():
331        if "level" in l:
332            l["level"] = bb.msg.stringToLevel(l["level"])
333
334    conf = logging.config.dictConfigClass(logconfig)
335    conf.configure()
336
337    # The user may have specified logging domains they want at a higher debug
338    # level than the standard.
339    for name, l in logconfig["loggers"].items():
340        if not name.startswith("BitBake."):
341            continue
342
343        if not "level" in l:
344            continue
345
346        curlevel = bb.msg.loggerDefaultDomains.get(name)
347        # Note: level parameter should already be a int because of conversion
348        # above
349        newlevel = int(l["level"])
350        if curlevel is None or newlevel < curlevel:
351            bb.msg.loggerDefaultDomains[name] = newlevel
352
353        # TODO: I don't think that setting the global log level should be necessary
354        #if newlevel < bb.msg.loggerDefaultLogLevel:
355        #    bb.msg.loggerDefaultLogLevel = newlevel
356
357    return conf
358