Coverage for emd/logger.py: 79%

122 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-09 10:07 +0000

1#!/usr/bin/python 

2 

3# vim: set expandtab ts=4 sw=4: 

4 

5""" 

6Routines for logging EMD analyses. 

7 

8Main Routines: 

9 set_up 

10 set_level 

11 get_level 

12 set_format 

13 enable 

14 disable 

15 is_active 

16 

17Decorators 

18 sift_logger 

19 wrap_verbose 

20 

21""" 

22 

23import logging 

24import logging.config 

25import sys 

26from functools import wraps 

27 

28import numpy as np 

29import yaml 

30 

31from .support import get_install_dir, get_installed_version 

32 

33# Housekeeping for logging 

34# Add a single null handler until set-up is called, this is activated on import 

35# to __init__ 

36logging.getLogger('emd').addHandler(logging.NullHandler()) 

37 

38# Initialise logging for this sub-module 

39logger = logging.getLogger(__name__) 

40 

41#%% ------------------------------------------------------------ 

42 

43levels = {'CRITICAL': 50, 

44 'ERROR': 40, 

45 'WARNING': 30, 

46 'INFO': 20, 

47 'VERBOSE': 15, 

48 'DEBUG': 10, 

49 'NOTSET': 0} 

50 

51 

52def add_logging_level(levelName, levelNum, methodName=None): 

53 """Add new level to the `logging` module and the current logging class. 

54 

55 Taken from - https://stackoverflow.com/a/35804945 

56 

57 `levelName` becomes an attribute of the `logging` module with the value 

58 `levelNum`. `methodName` becomes a convenience method for both `logging` 

59 itself and the class returned by `logging.getLoggerClass()` (usually just 

60 `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is 

61 used. 

62 

63 To avoid accidental clobberings of existing attributes, this method will 

64 raise an `AttributeError` if the level name is already an attribute of the 

65 `logging` module or if the method name is already present 

66 

67 Example 

68 ------- 

69 >>> addLoggingLevel('TRACE', logging.DEBUG - 5) 

70 >>> logging.getLogger(__name__).setLevel("TRACE") 

71 >>> logging.getLogger(__name__).trace('that worked') 

72 >>> logging.trace('so did this') 

73 >>> logging.TRACE 

74 

75 """ 

76 if not methodName: 

77 methodName = levelName.lower() 

78 

79 if hasattr(logging, levelName): 

80 raise AttributeError('{} already defined in logging module'.format(levelName)) 

81 if hasattr(logging, methodName): 

82 raise AttributeError('{} already defined in logging module'.format(methodName)) 

83 if hasattr(logging.getLoggerClass(), methodName): 

84 raise AttributeError('{} already defined in logger class'.format(methodName)) 

85 

86 # This method was inspired by the answers to Stack Overflow post 

87 # http://stackoverflow.com/q/2183233/2988730, especially 

88 # http://stackoverflow.com/a/13638084/2988730 

89 def log_for_level(self, message, *args, **kwargs): 

90 if self.isEnabledFor(levelNum): 

91 self._log(levelNum, message, args, **kwargs) 

92 

93 def log_to_root(message, *args, **kwargs): 

94 logging.log(levelNum, message, *args, **kwargs) 

95 

96 logging.addLevelName(levelNum, levelName) 

97 setattr(logging, levelName, levelNum) 

98 setattr(logging.getLoggerClass(), methodName, log_for_level) 

99 setattr(logging, methodName, log_to_root) 

100 

101#%% ------------------------------------------------------------ 

102 

103 

104default_config = """ 

105version: 1 

106loggers: 

107 emd: 

108 level: DEBUG 

109 handlers: [console, file] 

110 propagate: false 

111 

112handlers: 

113 console: 

114 class : logging.StreamHandler 

115 formatter: brief 

116 level : DEBUG 

117 stream : ext://sys.stdout 

118 file: 

119 class : logging.handlers.RotatingFileHandler 

120 formatter: verbose 

121 filename: {log_file} 

122 backupCount: 3 

123 maxBytes: 102400 

124 

125formatters: 

126 brief: 

127 format: '{prefix} %(message)s' 

128 default: 

129 format: '[%(asctime)s] {prefix} %(levelname)-8s %(funcName)20s : %(message)s' 

130 datefmt: '%H:%M:%S' 

131 verbose: 

132 format: '[%(asctime)s] {prefix} - %(levelname)s - emd.%(module)s:%(lineno)s - %(funcName)20s() : %(message)s' 

133 datefmt: '%Y-%m-%d %H:%M:%S' 

134 

135disable_existing_loggers: true 

136 

137""" 

138 

139 

140def set_up(prefix='', log_file='', level=None, console_format=None): 

141 """Initialise the EMD module logger. 

142 

143 Parameters 

144 ---------- 

145 prefix : str 

146 Optional prefix to attach to logger output 

147 log_file : str 

148 Optional path to a log file to record logger output 

149 level : {'CRITICAL', 'WARNING', 'INFO', 'DEBUG'} 

150 String indicating initial logging level 

151 console_format : str 

152 Formatting string for console logging. 

153 

154 """ 

155 # Format config with user options 

156 if (len(prefix) > 0) and (console_format != 'verbose'): 

157 prefix = prefix + ' :' 

158 new_config = default_config.format(prefix=prefix, log_file=log_file) 

159 # Load config to dict 

160 new_config = yaml.load(new_config, Loader=yaml.FullLoader) 

161 

162 # Remove log file from dict if not user requested 

163 if len(log_file) == 0: 

164 new_config['loggers']['emd']['handlers'] = ['console'] 

165 del new_config['handlers']['file'] 

166 

167 # Configure logger with dict 

168 logging.config.dictConfig(new_config) 

169 

170 if hasattr(logging, 'VERBOSE') is False: 

171 add_logging_level('VERBOSE', logging.INFO - 5) 

172 

173 # Customise options 

174 if level is not None: 

175 set_level(level) 

176 if console_format is not None: 

177 set_format(formatter=console_format, prefix=prefix) 

178 

179 # Say hello 

180 logger.info('EMD Logger Started') 

181 

182 # Print some info 

183 if len(log_file) > 0: 

184 logger.info('logging to file: {0}'.format(log_file)) 

185 logger.verbose('EMD v{0} installed in {1}'.format(get_installed_version, 

186 get_install_dir())) 

187 

188 

189def set_level(level, handler='console'): 

190 """Set new logging level for EMD module.""" 

191 logger = logging.getLogger('emd') 

192 for handler in logger.handlers: 

193 if handler.get_name() == 'console': 

194 if level in ['INFO', 'DEBUG']: 

195 logger.info("EMD logger: handler '{0}' level set to '{1}'".format(handler.get_name(), level)) 

196 handler.setLevel(getattr(logging, level)) 

197 

198 

199def get_level(handler='console'): 

200 """Return current logging level for EMD module.""" 

201 logger = logging.getLogger('emd') 

202 for handler in logger.handlers: 

203 if handler.get_name() == 'console': 

204 return handler.level 

205 

206 

207def set_format(formatter='', handler_name='console', prefix=''): 

208 """Set new formatter EMD module logger.""" 

209 logger = logging.getLogger('emd') 

210 new_config = yaml.load(default_config, Loader=yaml.FullLoader) 

211 try: 

212 fmtstr = new_config['formatters'][formatter]['format'] 

213 except KeyError: 

214 logger.warning("EMD logger format type '{0}' not recognised".format(formatter)) 

215 raise KeyError("EMD logger format type '{0}' not recognised".format(formatter)) 

216 fmt = logging.Formatter(fmtstr.format(prefix=prefix)) 

217 for handler in logger.handlers: 

218 if handler.get_name() == handler_name: 

219 handler.setFormatter(fmt) 

220 logger.info('EMD logger: handler {0} format changed to {1}'.format(handler_name, formatter)) 

221 

222 

223def disable(): 

224 """Turn off logging for the EMD module.""" 

225 logger = logging.getLogger('emd') 

226 logger.info('EMD logging disabled') 

227 logging.disable(sys.maxsize) 

228 

229 

230def enable(): 

231 """Turn on logging for the EMD module.""" 

232 logger = logging.getLogger('emd') 

233 logging.disable(logging.NOTSET) 

234 logger.info('EMD logging enabled') 

235 

236 

237def is_active(): 

238 """Return current logging level for EMD module.""" 

239 logger = logging.getLogger('emd') 

240 # Check if we have only a single NullHandler (which is default until set_up 

241 # is called) 

242 if len(logger.handlers) == 1 and isinstance(logger.handlers[0], logging.NullHandler): 

243 return False # Logger not initialised 

244 

245 # Or just return the disabled status 

246 return logger.disabled is False 

247 

248 

249# ------------------------------------ 

250 

251# Decorator for logging sift function 

252def sift_logger(sift_name): 

253 """Log sift function and inputs.""" 

254 # This first layer is a wrapper func to allow an argument to be passed in. 

255 # If we don't do this then we can't easily tell which function is being 

256 # decorated 

257 def add_logger(func): 

258 # This is the actual decorator 

259 @wraps(func) 

260 def sift_logger(*args, **kwargs): 

261 logger.info('STARTED: {0}'.format(sift_name)) 

262 

263 if sift_name == ('ensemble_sift', 'complete_ensemble_sift'): 

264 # Print number of ensembles if ensemble sift 

265 logger.debug('Input data size: {0}'.format(args[0].shape)) 

266 if 'nensembles' in kwargs: 

267 logger.debug('Computing {0} ensembles'.format(kwargs['nensembles'])) 

268 else: 

269 logger.debug('Computing 4 ensembles (default)') 

270 else: 

271 logger.debug('Input data size: {0}'.format(args[0].shape[0])) 

272 

273 # Print main keyword arguments 

274 logger.debug('Input Sift Args: {0}'.format(kwargs)) 

275 

276 # Call function itself 

277 func_output = func(*args, **kwargs) 

278 

279 # Print number of IMFs, catching other outputs if they're returned 

280 # as well 

281 if isinstance(func_output, np.ndarray): 

282 logger.debug('Returning {0} imfs'.format(func_output.shape[1])) 

283 else: 

284 logger.debug('Returning {0} imfs'.format(func_output[0].shape[1])) 

285 

286 # Close function 

287 logger.info('COMPLETED: {0}'.format(sift_name)) 

288 return func_output 

289 return sift_logger 

290 return add_logger 

291 

292 

293# Decorator for logging sift function 

294def wrap_verbose(func): 

295 """Add option to change logging level for single function calls.""" 

296 # This is the actual decorator 

297 @wraps(func) 

298 def inner_verbose(*args, **kwargs): 

299 

300 if ('verbose' in kwargs) and (kwargs['verbose'] is not None): 

301 tmp_level = kwargs['verbose'] 

302 current_level = get_level() 

303 set_level(level=tmp_level) 

304 elif ('verbose' in kwargs) and (kwargs['verbose'] is None): 

305 # Don't do anything 

306 pass 

307 elif ('verbose' in kwargs): 

308 logger.warning("Logger level '{0}' not recognised - level is unchanged".format(kwargs['verbose'])) 

309 

310 # Call function itself 

311 func_output = func(*args, **kwargs) 

312 

313 if ('verbose' in kwargs) and (kwargs['verbose'] is not None): 

314 set_level(level=logging._levelToName[current_level]) 

315 

316 return func_output 

317 return inner_verbose