Mercurial > p > roundup > code
changeset 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 | 0bb29d0509c9 |
| children | e024fbb86f90 |
| files | CHANGES.txt doc/admin_guide.txt roundup/configuration.py test/test_config.py |
| diffstat | 4 files changed, 107 insertions(+), 4 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES.txt Mon Sep 01 20:35:54 2025 -0400 +++ b/CHANGES.txt Mon Sep 01 21:54:48 2025 -0400 @@ -42,6 +42,8 @@ - add readline command to roundup-admin to list history, control input mode etc. Also support bang (!) commands to rerun commands in history or put them in the input buffer for editing. (John Rouillard) +- add format to logging section in config.ini. Used to set default + logging format. (John Rouillard) 2025-07-13 2.5.0
--- a/doc/admin_guide.txt Mon Sep 01 20:35:54 2025 -0400 +++ b/doc/admin_guide.txt Mon Sep 01 21:54:48 2025 -0400 @@ -63,6 +63,8 @@ - tracker configuration file lets you disable other loggers (e.g. when running under a wsgi framework) with ``logging`` -> ``disable_loggers``. + - tracker configuration file can set the log format using + ``logging`` -> ``format``. See :ref:`logFormat` for more info. - ``roundup-server`` specifies the location of a log file on the command line - ``roundup-server`` enable using the standard python logger with @@ -83,10 +85,26 @@ In both cases, if no logfile is specified then logging will simply be sent to sys.stderr with only logging of ERROR messages. +.. _logFormat: + +Defining the Log Format +----------------------- + +Starting with Roundup 2.6 you can specify the logging format. In the +``logging`` -> ``format`` setting of config.ini you can use any of the +`standard logging LogRecord attributes +<https://docs.python.org/3/library/logging.html#logrecord-attributes>`_. +However you must double any ``%`` format markers. The default value +is:: + + %%(asctime)s %%(levelname)s %%(message)s + Standard Logging Setup ---------------------- -You can specify your log configs in one of two formats: +If the settings in config.ini are not sufficient for your logging +requirements, you can specify a full logging configuration in one of +two formats: * `fileConfig format <https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig>`_
--- a/roundup/configuration.py Mon Sep 01 20:35:54 2025 -0400 +++ b/roundup/configuration.py Mon Sep 01 21:54:48 2025 -0400 @@ -273,7 +273,8 @@ try: if config.has_option(self.section, self.setting): self.set(config.get(self.section, self.setting)) - except configparser.InterpolationSyntaxError as e: + except (configparser.InterpolationSyntaxError, + configparser.InterpolationMissingOptionError) as e: raise ParsingOptionError( _("Error in %(filepath)s with section [%(section)s] at " "option %(option)s: %(message)s") % { @@ -575,6 +576,48 @@ return None +class LoggingFormatOption(Option): + """Replace escaped % (as %%) with single %. + + Config file parsing allows variable interpolation using + %(keyname)s. However this is exactly the format that we need + for creating a logging format string. So we tell the user to + quote the string using %%(...). Then we turn %%( -> %( when + retrieved. + """ + + class_description = ("Allowed value: Python logging module named " + "attributes with % sign doubled.") + + def str2value(self, value): + """Check format of unquoted string looking for missing specifiers. + + This does a dirty check to see if a token is missing a + specifier. So "%(ascdate)s %(level) " would fail because of + the 's' missing after 'level)'. But "%(ascdate)s %(level)s" + would pass. + + Note that %(foo)s generates a error from the ini parser + with a less than wonderful message. + """ + unquoted_val = value.replace("%%(", "%(") + + # regexp matches all current logging record object attribute names. + scanned_result = re.sub(r'%\([A-Za-z_]+\)\S','', unquoted_val ) + if scanned_result.find('%(') != -1: + raise OptionValueError( + self, unquoted_val, + "Check that all substitution tokens have a format " + "specifier after the ). Unrecognized use of %%(...) in: " + "%s" % scanned_result) + + return str(unquoted_val) + + def _value2str(self, value): + """Replace %( with %%( to quote the format substitution param. + """ + return value.replace("%(", "%%(") + class OriginHeadersListOption(Option): """List of space seperated origin header values. @@ -1614,6 +1657,10 @@ "Minimal severity level of messages written to log file.\n" "If above 'config' option is set, this option has no effect.\n" "Allowed values: DEBUG, INFO, WARNING, ERROR"), + (LoggingFormatOption, "format", + "%(asctime)s %(levelname)s %(message)s", + "Format of the logging messages with all '%' signs\n" + "doubled so they are not interpreted by the config file."), (BooleanOption, "disable_loggers", "no", "If set to yes, only the loggers configured in this section will\n" "be used. Yes will disable gunicorn's --access-logfile.\n"), @@ -2448,8 +2495,7 @@ hdlr = logging.FileHandler(_file) if _file else \ logging.StreamHandler(sys.stdout) - formatter = logging.Formatter( - '%(asctime)s %(levelname)s %(message)s') + formatter = logging.Formatter(self["LOGGING_FORMAT"]) hdlr.setFormatter(formatter) # no logging API to remove all existing handlers!?! for h in logger.handlers:
--- a/test/test_config.py Mon Sep 01 20:35:54 2025 -0400 +++ b/test/test_config.py Mon Sep 01 21:54:48 2025 -0400 @@ -1112,6 +1112,43 @@ self.assertIn("nati", string_rep) self.assertIn("'whoosh'", string_rep) + def testLoggerFormat(self): + config = configuration.CoreConfig() + + # verify config is initalized to defaults + self.assertEqual(config['LOGGING_FORMAT'], + '%(asctime)s %(levelname)s %(message)s') + + # load config + config.load(self.dirname) + self.assertEqual(config['LOGGING_FORMAT'], + '%(asctime)s %(levelname)s %(message)s') + + # break config using an incomplete format specifier (no trailing 's') + self.munge_configini(mods=[ ("format = ", "%%(asctime)s %%(levelname) %%(message)s") ], section="[logging]") + + # load config + with self.assertRaises(configuration.OptionValueError) as cm: + config.load(self.dirname) + + self.assertIn('Unrecognized use of %(...) in: %(levelname)', + cm.exception.args[2]) + + # break config by not dubling % sign to quote it from configparser + self.munge_configini(mods=[ ("format = ", "%(asctime)s %%(levelname) %%(message)s") ], section="[logging]") + + with self.assertRaises( + configuration.ParsingOptionError) as cm: + config.load(self.dirname) + + self.assertEqual(cm.exception.args[0], + "Error in _test_instance/config.ini with section " + "[logging] at option format: Bad value substitution: " + "option 'format' in section 'logging' contains an " + "interpolation key 'asctime' which is not a valid " + "option name. Raw value: '%(asctime)s %%(levelname) " + "%%(message)s'") + def testDictLoggerConfigViaJson(self): # good base test case
