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