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