Skip to content

Commit 27162c6

Browse files
committed
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.
1 parent b8e186c commit 27162c6

File tree

4 files changed

+107
-4
lines changed

4 files changed

+107
-4
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Features:
4242
- add readline command to roundup-admin to list history, control input
4343
mode etc. Also support bang (!) commands to rerun commands in history
4444
or put them in the input buffer for editing. (John Rouillard)
45+
- add format to logging section in config.ini. Used to set default
46+
logging format. (John Rouillard)
4547

4648
2025-07-13 2.5.0
4749

doc/admin_guide.txt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ Configuration for "BasicLogging" implementation:
6363
- tracker configuration file lets you disable other loggers
6464
(e.g. when running under a wsgi framework) with
6565
``logging`` -> ``disable_loggers``.
66+
- tracker configuration file can set the log format using
67+
``logging`` -> ``format``. See :ref:`logFormat` for more info.
6668
- ``roundup-server`` specifies the location of a log file on the command
6769
line
6870
- ``roundup-server`` enable using the standard python logger with
@@ -83,10 +85,26 @@ Configuration for standard "logging" module:
8385
In both cases, if no logfile is specified then logging will simply be sent
8486
to sys.stderr with only logging of ERROR messages.
8587

88+
.. _logFormat:
89+
90+
Defining the Log Format
91+
-----------------------
92+
93+
Starting with Roundup 2.6 you can specify the logging format. In the
94+
``logging`` -> ``format`` setting of config.ini you can use any of the
95+
`standard logging LogRecord attributes
96+
<https://docs.python.org/3/library/logging.html#logrecord-attributes>`_.
97+
However you must double any ``%`` format markers. The default value
98+
is::
99+
100+
%%(asctime)s %%(levelname)s %%(message)s
101+
86102
Standard Logging Setup
87103
----------------------
88104

89-
You can specify your log configs in one of two formats:
105+
If the settings in config.ini are not sufficient for your logging
106+
requirements, you can specify a full logging configuration in one of
107+
two formats:
90108

91109
* `fileConfig format
92110
<https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig>`_

roundup/configuration.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ def load_ini(self, config):
273273
try:
274274
if config.has_option(self.section, self.setting):
275275
self.set(config.get(self.section, self.setting))
276-
except configparser.InterpolationSyntaxError as e:
276+
except (configparser.InterpolationSyntaxError,
277+
configparser.InterpolationMissingOptionError) as e:
277278
raise ParsingOptionError(
278279
_("Error in %(filepath)s with section [%(section)s] at "
279280
"option %(option)s: %(message)s") % {
@@ -575,6 +576,48 @@ def get(self):
575576
return None
576577

577578

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("%(", "%%(")
620+
578621
class OriginHeadersListOption(Option):
579622

580623
"""List of space seperated origin header values.
@@ -1614,6 +1657,10 @@ def str2value(self, value):
16141657
"Minimal severity level of messages written to log file.\n"
16151658
"If above 'config' option is set, this option has no effect.\n"
16161659
"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."),
16171664
(BooleanOption, "disable_loggers", "no",
16181665
"If set to yes, only the loggers configured in this section will\n"
16191666
"be used. Yes will disable gunicorn's --access-logfile.\n"),
@@ -2448,8 +2495,7 @@ def init_logging(self):
24482495
hdlr = logging.FileHandler(_file) if _file else \
24492496
logging.StreamHandler(sys.stdout)
24502497

2451-
formatter = logging.Formatter(
2452-
'%(asctime)s %(levelname)s %(message)s')
2498+
formatter = logging.Formatter(self["LOGGING_FORMAT"])
24532499
hdlr.setFormatter(formatter)
24542500
# no logging API to remove all existing handlers!?!
24552501
for h in logger.handlers:

test/test_config.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,43 @@ def testInvalidIndexerValue(self):
11121112
self.assertIn("nati", string_rep)
11131113
self.assertIn("'whoosh'", string_rep)
11141114

1115+
def testLoggerFormat(self):
1116+
config = configuration.CoreConfig()
1117+
1118+
# verify config is initalized to defaults
1119+
self.assertEqual(config['LOGGING_FORMAT'],
1120+
'%(asctime)s %(levelname)s %(message)s')
1121+
1122+
# load config
1123+
config.load(self.dirname)
1124+
self.assertEqual(config['LOGGING_FORMAT'],
1125+
'%(asctime)s %(levelname)s %(message)s')
1126+
1127+
# break config using an incomplete format specifier (no trailing 's')
1128+
self.munge_configini(mods=[ ("format = ", "%%(asctime)s %%(levelname) %%(message)s") ], section="[logging]")
1129+
1130+
# load config
1131+
with self.assertRaises(configuration.OptionValueError) as cm:
1132+
config.load(self.dirname)
1133+
1134+
self.assertIn('Unrecognized use of %(...) in: %(levelname)',
1135+
cm.exception.args[2])
1136+
1137+
# break config by not dubling % sign to quote it from configparser
1138+
self.munge_configini(mods=[ ("format = ", "%(asctime)s %%(levelname) %%(message)s") ], section="[logging]")
1139+
1140+
with self.assertRaises(
1141+
configuration.ParsingOptionError) as cm:
1142+
config.load(self.dirname)
1143+
1144+
self.assertEqual(cm.exception.args[0],
1145+
"Error in _test_instance/config.ini with section "
1146+
"[logging] at option format: Bad value substitution: "
1147+
"option 'format' in section 'logging' contains an "
1148+
"interpolation key 'asctime' which is not a valid "
1149+
"option name. Raw value: '%(asctime)s %%(levelname) "
1150+
"%%(message)s'")
1151+
11151152
def testDictLoggerConfigViaJson(self):
11161153

11171154
# good base test case

0 commit comments

Comments
 (0)