xref: /openbmc/openbmc/poky/bitbake/lib/bb/msg.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
1 """
2 BitBake 'msg' implementation
3 
4 Message handling infrastructure for bitbake
5 
6 """
7 
8 # Copyright (C) 2006        Richard Purdie
9 #
10 # SPDX-License-Identifier: GPL-2.0-only
11 #
12 
13 import sys
14 import copy
15 import logging
16 import logging.config
17 import os
18 from itertools import groupby
19 import bb
20 import bb.event
21 
22 class 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 
108 class 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 
126 class 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 
142 class 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 
153 class 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 
167 loggerDefaultLogLevel = BBLogFormatter.NOTE
168 loggerDefaultDomains = {}
169 
170 def 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 
187 def constructLogOptions():
188     return loggerDefaultLogLevel, loggerDefaultDomains
189 
190 def 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 
198 def 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 
215 def 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 
223 def 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 
239 def 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 
246 def 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 
293 def 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