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