comparison test/test_config.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 1e38ca6fb16e
children 4a948ad46579
comparison
equal deleted inserted replaced
8422:e97cae093746 8423:94eed885e958
23 import shutil 23 import shutil
24 import sys 24 import sys
25 import unittest 25 import unittest
26 26
27 from os.path import normpath 27 from os.path import normpath
28 from textwrap import dedent
28 29
29 from roundup import configuration 30 from roundup import configuration
30 from roundup.backends import get_backend, have_backend 31 from roundup.backends import get_backend, have_backend
31 from roundup.hyperdb import DatabaseError 32 from roundup.hyperdb import DatabaseError
32 33
1044 # verify that args show up in string representaton 1045 # verify that args show up in string representaton
1045 string_rep = cm.exception.__str__() 1046 string_rep = cm.exception.__str__()
1046 print(string_rep) 1047 print(string_rep)
1047 self.assertIn("nati", string_rep) 1048 self.assertIn("nati", string_rep)
1048 self.assertIn("'whoosh'", string_rep) 1049 self.assertIn("'whoosh'", string_rep)
1050
1051 def testDictLoggerConfigViaJson(self):
1052
1053 # test case broken, comment on version line misformatted
1054 config1 = dedent("""
1055 {
1056 "version": 1, # only supported version
1057 "disable_existing_loggers": false, # keep the wsgi loggers
1058
1059 "formatters": {
1060 # standard Roundup formatter including context id.
1061 "standard": {
1062 "format": "%(asctime)s %(levelname)s %(name)s:%(module)s %(msg)s"
1063 },
1064 # used for waitress wsgi server to produce httpd style logs
1065 "http": {
1066 "format": "%(message)s"
1067 }
1068 },
1069 "handlers": {
1070 # create an access.log style http log file
1071 "access": {
1072 "level": "INFO",
1073 "formatter": "http",
1074 "class": "logging.FileHandler",
1075 "filename": "demo/access.log"
1076 },
1077 # logging for roundup.* loggers
1078 "roundup": {
1079 "level": "DEBUG",
1080 "formatter": "standard",
1081 "class": "logging.FileHandler",
1082 "filename": "demo/roundup.log"
1083 },
1084 # print to stdout - fall through for other logging
1085 "default": {
1086 "level": "DEBUG",
1087 "formatter": "standard",
1088 "class": "logging.StreamHandler",
1089 "stream": "ext://sys.stdout"
1090 }
1091 },
1092 "loggers": {
1093 "": {
1094 "handlers": [
1095 "default" # used by wsgi/usgi
1096 ],
1097 "level": "DEBUG",
1098 "propagate": false
1099 },
1100 # used by roundup.* loggers
1101 "roundup": {
1102 "handlers": [
1103 "roundup"
1104 ],
1105 "level": "DEBUG",
1106 "propagate": false # note pytest testing with caplog requires
1107 # this to be true
1108 },
1109 "roundup.hyperdb": {
1110 "handlers": [
1111 "roundup"
1112 ],
1113 "level": "INFO", # can be a little noisy INFO for production
1114 "propagate": false
1115 },
1116 "roundup.wsgi": { # using the waitress framework
1117 "handlers": [
1118 "roundup"
1119 ],
1120 "level": "DEBUG",
1121 "propagate": false
1122 },
1123 "roundup.wsgi.translogger": { # httpd style logging
1124 "handlers": [
1125 "access"
1126 ],
1127 "level": "DEBUG",
1128 "propagate": false
1129 },
1130 "root": {
1131 "handlers": [
1132 "default"
1133 ],
1134 "level": "DEBUG",
1135 "propagate": false
1136 }
1137 }
1138 }
1139 """)
1140
1141 log_config_filename = self.instance.tracker_home \
1142 + "/_test_log_config.json"
1143
1144 # happy path
1145 with open(log_config_filename, "w") as log_config_file:
1146 log_config_file.write(config1)
1147
1148 config = self.db.config.load_config_dict_from_json_file(
1149 log_config_filename)
1150 self.assertIn("version", config)
1151 self.assertEqual(config['version'], 1)
1152
1153 # broken inline comment misformatted
1154 test_config = config1.replace(": 1, #", ": 1, #")
1155 with open(log_config_filename, "w") as log_config_file:
1156 log_config_file.write(test_config)
1157
1158 with self.assertRaises(configuration.LoggingConfigError) as cm:
1159 config = self.db.config.load_config_dict_from_json_file(
1160 log_config_filename)
1161 self.assertEqual(
1162 cm.exception.args[0],
1163 ('Error parsing json logging dict '
1164 '(_test_instance/_test_log_config.json) near \n\n '
1165 '"version": 1, # only supported version\n\nExpecting '
1166 'property name enclosed in double quotes: line 3 column 18.\n'
1167 'Maybe bad inline comment, 3 spaces needed before #.')
1168 )
1169
1170 # broken trailing , on last dict element
1171 test_config = config1.replace(' "ext://sys.stdout"',
1172 ' "ext://sys.stdout",'
1173 )
1174 with open(log_config_filename, "w") as log_config_file:
1175 log_config_file.write(test_config)
1176
1177 with self.assertRaises(configuration.LoggingConfigError) as cm:
1178 config = self.db.config.load_config_dict_from_json_file(
1179 log_config_filename)
1180 self.assertEqual(
1181 cm.exception.args[0],
1182 ('Error parsing json logging dict '
1183 '(_test_instance/_test_log_config.json) near \n\n'
1184 ' }\n\nExpecting property name enclosed in double '
1185 'quotes: line 37 column 6.')
1186 )
1187
1188 # happy path for init_logging()
1189
1190 # verify preconditions
1191 logger = logging.getLogger("roundup")
1192 self.assertEqual(logger.level, 40) # error default from config.ini
1193 self.assertEqual(logger.filters, [])
1194
1195 with open(log_config_filename, "w") as log_config_file:
1196 log_config_file.write(config1)
1197
1198 # file is made relative to tracker dir.
1199 self.db.config["LOGGING_CONFIG"] = '_test_log_config.json'
1200 config = self.db.config.init_logging()
1201 self.assertIs(config, None)
1202
1203 logger = logging.getLogger("roundup")
1204 self.assertEqual(logger.level, 10) # debug
1205 self.assertEqual(logger.filters, [])
1206
1207 # broken invalid format type (int not str)
1208 test_config = config1.replace('"format": "%(message)s"',
1209 '"format": 1234',)
1210 with open(log_config_filename, "w") as log_config_file:
1211 log_config_file.write(test_config)
1212
1213 # file is made relative to tracker dir.
1214 self.db.config["LOGGING_CONFIG"] = '_test_log_config.json'
1215 with self.assertRaises(configuration.LoggingConfigError) as cm:
1216 config = self.db.config.init_logging()
1217 self.assertEqual(
1218 cm.exception.args[0],
1219 ('Error loading logging dict from '
1220 '_test_instance/_test_log_config.json.\n'
1221 "ValueError: Unable to configure formatter 'http'\n"
1222 'expected string or bytes-like object\n')
1223 )
1224
1225 # broken invalid level MANGO
1226 test_config = config1.replace(
1227 ': "INFO", # can',
1228 ': "MANGO", # can')
1229 with open(log_config_filename, "w") as log_config_file:
1230 log_config_file.write(test_config)
1231
1232 # file is made relative to tracker dir.
1233 self.db.config["LOGGING_CONFIG"] = '_test_log_config.json'
1234 with self.assertRaises(configuration.LoggingConfigError) as cm:
1235 config = self.db.config.init_logging()
1236 self.assertEqual(
1237 cm.exception.args[0],
1238 ("Error loading logging dict from "
1239 "_test_instance/_test_log_config.json.\nValueError: "
1240 "Unable to configure logger 'roundup.hyperdb'\nUnknown level: "
1241 "'MANGO'\n")
1242
1243 )

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