comparison roundup/configuration.py @ 8423:94eed885e958

feat: add support for using dictConfig to configure logging. Basic logging config (one level and one output file non-rotating) was always possible from config.ini. However the LOGGING_CONFIG setting could be used to load an ini fileConfig style file to set various channels (e.g. roundup.hyperdb) (also called qualname or tags) with their own logging level, destination (rotating file, socket, /dev/null) and log format. This is now a deprecated method in newer logging modules. The dictConfig format is preferred and allows disabiling other loggers as well as invoking new loggers in local code. This commit adds support for it reading the dict from a .json file. It also implements a comment convention so you can document the dictConfig. configuration.py: new code test_config.py: test added for the new code. admin_guide.txt, upgrading.txt CHANGES.txt: docs added upgrading references the section in admin_guid.
author John Rouillard <rouilj@ieee.org>
date Tue, 19 Aug 2025 22:32:46 -0400
parents 7ff47307b4b1
children de1dac9abcb6
comparison
equal deleted inserted replaced
8422:e97cae093746 8423:94eed885e958
37 class ConfigurationError(RoundupException): 37 class ConfigurationError(RoundupException):
38 pass 38 pass
39 39
40 40
41 class ParsingOptionError(ConfigurationError): 41 class ParsingOptionError(ConfigurationError):
42 def __str__(self):
43 return self.args[0]
44
45
46 class LoggingConfigError(ConfigurationError):
47 def __init__(self, message, **attrs):
48 super().__init__(message)
49 for key, value in attrs.items():
50 self.__setattr__(key, value)
51
42 def __str__(self): 52 def __str__(self):
43 return self.args[0] 53 return self.args[0]
44 54
45 55
46 class NoConfigError(ConfigurationError): 56 class NoConfigError(ConfigurationError):
2328 self.ext.reset() 2338 self.ext.reset()
2329 if self.detectors: 2339 if self.detectors:
2330 self.detectors.reset() 2340 self.detectors.reset()
2331 self.init_logging() 2341 self.init_logging()
2332 2342
2343 def load_config_dict_from_json_file(self, filename):
2344 import json
2345 comment_re = re.compile(
2346 r"""^\s*\#.* # comment at beginning of line possibly indented.
2347 | # or
2348 ^(.*)\s\s\s\#.* # comment char preceeded by at least three spaces.
2349 """, re.VERBOSE)
2350
2351 config_list = []
2352 with open(filename) as config_file:
2353 for line in config_file:
2354 match = comment_re.search(line)
2355 if match:
2356 if match.lastindex:
2357 config_list.append(match.group(1) + "\n")
2358 else:
2359 # insert blank line for comment line to
2360 # keep line numbers in sync.
2361 config_list.append("\n")
2362 continue
2363 config_list.append(line)
2364
2365 try:
2366 config_dict = json.loads("".join(config_list))
2367 except json.decoder.JSONDecodeError as e:
2368 error_at_doc_line = e.lineno
2369 # subtract 1 - zero index on config_list
2370 # remove '\n' for display
2371 line = config_list[error_at_doc_line - 1][:-1]
2372
2373 hint = ""
2374 if line.find('#') != -1:
2375 hint = "\nMaybe bad inline comment, 3 spaces needed before #."
2376
2377 raise LoggingConfigError(
2378 'Error parsing json logging dict (%(file)s) '
2379 'near \n\n %(line)s\n\n'
2380 '%(msg)s: line %(lineno)s column %(colno)s.%(hint)s' %
2381 {"file": filename,
2382 "line": line,
2383 "msg": e.msg,
2384 "lineno": error_at_doc_line,
2385 "colno": e.colno,
2386 "hint": hint},
2387 config_file=self.filepath,
2388 source="json.loads"
2389 )
2390
2391 return config_dict
2392
2333 def init_logging(self): 2393 def init_logging(self):
2334 _file = self["LOGGING_CONFIG"] 2394 _file = self["LOGGING_CONFIG"]
2335 if _file and os.path.isfile(_file): 2395 if _file and os.path.isfile(_file) and _file.endswith(".ini"):
2336 logging.config.fileConfig( 2396 logging.config.fileConfig(
2337 _file, 2397 _file,
2338 disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"]) 2398 disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"])
2399 return
2400
2401 if _file and os.path.isfile(_file) and _file.endswith(".json"):
2402 config_dict = self.load_config_dict_from_json_file(_file)
2403 try:
2404 logging.config.dictConfig(config_dict)
2405 except ValueError as e:
2406 # docs say these exceptions:
2407 # ValueError, TypeError, AttributeError, ImportError
2408 # could be raised, but
2409 # looking through the code, it looks like
2410 # configure() maps all exceptions (including
2411 # ImportError, TypeError) raised by functions to
2412 # ValueError.
2413 context = "No additional information available."
2414 if hasattr(e, '__context__') and e.__context__:
2415 # get additional error info. E.G. if INFO
2416 # is replaced by MANGO, context is:
2417 # ValueError("Unknown level: 'MANGO'")
2418 # while str(e) is "Unable to configure handler 'access'"
2419 context = e.__context__
2420
2421 raise LoggingConfigError(
2422 'Error loading logging dict from %(file)s.\n'
2423 '%(msg)s\n%(context)s\n' % {
2424 "file": _file,
2425 "msg": type(e).__name__ + ": " + str(e),
2426 "context": context
2427 },
2428 config_file=self.filepath,
2429 source="dictConfig"
2430 )
2431
2339 return 2432 return
2340 2433
2341 _file = self["LOGGING_FILENAME"] 2434 _file = self["LOGGING_FILENAME"]
2342 # set file & level on the roundup logger 2435 # set file & level on the roundup logger
2343 logger = logging.getLogger('roundup') 2436 logger = logging.getLogger('roundup')

Roundup Issue Tracker: http://roundup-tracker.org/