Mercurial > p > roundup > code
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') |
