comparison roundup/configuration.py @ 8443:39a6825d10ca

feat: allow admin to set logging format from config.ini This is prep work for adding a per thread logging variable that can be used to tie all logs for a single request together. This uses the same default logging format as before, just moves it to config.ini. Also because of configparser, the logging format has to have doubled % signs. So use: %%(asctime)s not '%(asctime)s' as configparser tries to interpolate that string and asctime is not defined in the configparser's scope. Using %%(asctime)s is not interpolated by configparser and is passed into Roundup.
author John Rouillard <rouilj@ieee.org>
date Mon, 01 Sep 2025 21:54:48 -0400
parents 66284037142e
children 14c7c07b32d8
comparison
equal deleted inserted replaced
8442:0bb29d0509c9 8443:39a6825d10ca
271 def load_ini(self, config): 271 def load_ini(self, config):
272 """Load value from ConfigParser object""" 272 """Load value from ConfigParser object"""
273 try: 273 try:
274 if config.has_option(self.section, self.setting): 274 if config.has_option(self.section, self.setting):
275 self.set(config.get(self.section, self.setting)) 275 self.set(config.get(self.section, self.setting))
276 except configparser.InterpolationSyntaxError as e: 276 except (configparser.InterpolationSyntaxError,
277 configparser.InterpolationMissingOptionError) as e:
277 raise ParsingOptionError( 278 raise ParsingOptionError(
278 _("Error in %(filepath)s with section [%(section)s] at " 279 _("Error in %(filepath)s with section [%(section)s] at "
279 "option %(option)s: %(message)s") % { 280 "option %(option)s: %(message)s") % {
280 "filepath": self.config.filepath, 281 "filepath": self.config.filepath,
281 "section": e.section, 282 "section": e.section,
572 if pathlist: 573 if pathlist:
573 return pathlist 574 return pathlist
574 else: 575 else:
575 return None 576 return None
576 577
578
579 class LoggingFormatOption(Option):
580 """Replace escaped % (as %%) with single %.
581
582 Config file parsing allows variable interpolation using
583 %(keyname)s. However this is exactly the format that we need
584 for creating a logging format string. So we tell the user to
585 quote the string using %%(...). Then we turn %%( -> %( when
586 retrieved.
587 """
588
589 class_description = ("Allowed value: Python logging module named "
590 "attributes with % sign doubled.")
591
592 def str2value(self, value):
593 """Check format of unquoted string looking for missing specifiers.
594
595 This does a dirty check to see if a token is missing a
596 specifier. So "%(ascdate)s %(level) " would fail because of
597 the 's' missing after 'level)'. But "%(ascdate)s %(level)s"
598 would pass.
599
600 Note that %(foo)s generates a error from the ini parser
601 with a less than wonderful message.
602 """
603 unquoted_val = value.replace("%%(", "%(")
604
605 # regexp matches all current logging record object attribute names.
606 scanned_result = re.sub(r'%\([A-Za-z_]+\)\S','', unquoted_val )
607 if scanned_result.find('%(') != -1:
608 raise OptionValueError(
609 self, unquoted_val,
610 "Check that all substitution tokens have a format "
611 "specifier after the ). Unrecognized use of %%(...) in: "
612 "%s" % scanned_result)
613
614 return str(unquoted_val)
615
616 def _value2str(self, value):
617 """Replace %( with %%( to quote the format substitution param.
618 """
619 return value.replace("%(", "%%(")
577 620
578 class OriginHeadersListOption(Option): 621 class OriginHeadersListOption(Option):
579 622
580 """List of space seperated origin header values. 623 """List of space seperated origin header values.
581 """ 624 """
1612 "If above 'config' option is set, this option has no effect."), 1655 "If above 'config' option is set, this option has no effect."),
1613 (Option, "level", "ERROR", 1656 (Option, "level", "ERROR",
1614 "Minimal severity level of messages written to log file.\n" 1657 "Minimal severity level of messages written to log file.\n"
1615 "If above 'config' option is set, this option has no effect.\n" 1658 "If above 'config' option is set, this option has no effect.\n"
1616 "Allowed values: DEBUG, INFO, WARNING, ERROR"), 1659 "Allowed values: DEBUG, INFO, WARNING, ERROR"),
1660 (LoggingFormatOption, "format",
1661 "%(asctime)s %(levelname)s %(message)s",
1662 "Format of the logging messages with all '%' signs\n"
1663 "doubled so they are not interpreted by the config file."),
1617 (BooleanOption, "disable_loggers", "no", 1664 (BooleanOption, "disable_loggers", "no",
1618 "If set to yes, only the loggers configured in this section will\n" 1665 "If set to yes, only the loggers configured in this section will\n"
1619 "be used. Yes will disable gunicorn's --access-logfile.\n"), 1666 "be used. Yes will disable gunicorn's --access-logfile.\n"),
1620 )), 1667 )),
1621 ("mail", ( 1668 ("mail", (
2446 # set file & level on the roundup logger 2493 # set file & level on the roundup logger
2447 logger = logging.getLogger('roundup') 2494 logger = logging.getLogger('roundup')
2448 hdlr = logging.FileHandler(_file) if _file else \ 2495 hdlr = logging.FileHandler(_file) if _file else \
2449 logging.StreamHandler(sys.stdout) 2496 logging.StreamHandler(sys.stdout)
2450 2497
2451 formatter = logging.Formatter( 2498 formatter = logging.Formatter(self["LOGGING_FORMAT"])
2452 '%(asctime)s %(levelname)s %(message)s')
2453 hdlr.setFormatter(formatter) 2499 hdlr.setFormatter(formatter)
2454 # no logging API to remove all existing handlers!?! 2500 # no logging API to remove all existing handlers!?!
2455 for h in logger.handlers: 2501 for h in logger.handlers:
2456 h.close() 2502 h.close()
2457 logger.removeHandler(hdlr) 2503 logger.removeHandler(hdlr)

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