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