Coverage for emd/logger.py: 79%
122 statements
« prev ^ index » next coverage.py v7.6.11, created at 2025-03-08 15:44 +0000
« prev ^ index » next coverage.py v7.6.11, created at 2025-03-08 15:44 +0000
1#!/usr/bin/python
3# vim: set expandtab ts=4 sw=4:
5"""
6Routines for logging EMD analyses.
8Main Routines:
9 set_up
10 set_level
11 get_level
12 set_format
13 enable
14 disable
15 is_active
17Decorators
18 sift_logger
19 wrap_verbose
21"""
23import logging
24import logging.config
25import sys
26from functools import wraps
28import numpy as np
29import yaml
31from .support import get_install_dir, get_installed_version
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())
38# Initialise logging for this sub-module
39logger = logging.getLogger(__name__)
41#%% ------------------------------------------------------------
43levels = {'CRITICAL': 50,
44 'ERROR': 40,
45 'WARNING': 30,
46 'INFO': 20,
47 'VERBOSE': 15,
48 'DEBUG': 10,
49 'NOTSET': 0}
52def add_logging_level(levelName, levelNum, methodName=None):
53 """Add new level to the `logging` module and the current logging class.
55 Taken from - https://stackoverflow.com/a/35804945
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.
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
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
75 """
76 if not methodName:
77 methodName = levelName.lower()
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))
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)
93 def log_to_root(message, *args, **kwargs):
94 logging.log(levelNum, message, *args, **kwargs)
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)
101#%% ------------------------------------------------------------
104default_config = """
105version: 1
106loggers:
107 emd:
108 level: DEBUG
109 handlers: [console, file]
110 propagate: false
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
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'
135disable_existing_loggers: true
137"""
140def set_up(prefix='', log_file='', level=None, console_format=None):
141 """Initialise the EMD module logger.
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.
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)
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']
167 # Configure logger with dict
168 logging.config.dictConfig(new_config)
170 if hasattr(logging, 'VERBOSE') is False:
171 add_logging_level('VERBOSE', logging.INFO - 5)
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)
179 # Say hello
180 logger.info('EMD Logger Started')
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()))
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))
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
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))
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)
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')
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
245 # Or just return the disabled status
246 return logger.disabled is False
249# ------------------------------------
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))
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]))
273 # Print main keyword arguments
274 logger.debug('Input Sift Args: {0}'.format(kwargs))
276 # Call function itself
277 func_output = func(*args, **kwargs)
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]))
286 # Close function
287 logger.info('COMPLETED: {0}'.format(sift_name))
288 return func_output
289 return sift_logger
290 return add_logger
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):
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']))
310 # Call function itself
311 func_output = func(*args, **kwargs)
313 if ('verbose' in kwargs) and (kwargs['verbose'] is not None):
314 set_level(level=logging._levelToName[current_level])
316 return func_output
317 return inner_verbose