ActiveState Code

Recipe 576803: Run-Time Configurable Logging


The following Python code is an extension to the logging module by allowing logging configuration at run-time.

Python
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#------------------------------------------------------------------------------
# Copyright (c) 2009  Alex Omoto
# 
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.
#
#------------------------------------------------------------------------------

""" 

rt_logging - A run-time configurable extension to Python's 'logging' module.


Features: 

- logging configuration is re-loaded whenever settings file is modified
- logging configuration is persistent over python sessions
- transparent in usage (other than import)


Usage:

>>> # Usage is the same except we import rt_logging instead
>>> from rt_logging import *
>>>
>>> # Create a log config
>>> root = getLogger('root')
>>> log1 = getLogger('log1')
>>> log2 = getLogger('log1.log2')
>>>
>>> # Modify the level, handler, or format of log2 while this runs...
>>> # Default configuration file is logconf.ini
>>> for i in range(1000):
>>>     log2.info('testing')
>>>     import time; time.sleep(1)
>>>     
"""

# Python modules
import os
import sys
from logging import *
from ConfigParser import ConfigParser


#-------------------------------------------------------------
#  Global
#-------------------------------------------------------------

_logconf = os.path.join(os.path.dirname(__file__), 'logconf.ini')

#-------------------------------------------------------------
#  Run-Time Classes
#-------------------------------------------------------------

class RtLogger(Logger):
    """ Run-Time Logger class """  
    
    def __init__(self, *args, **kwargs):
        """ Initialize Logger Config file and settings """
        
        # Run Base Logger init
        ret = Logger.__init__(self, *args, **kwargs)
        
        # Setup a default logging config
        handler = StreamHandler()  
        handler.setFormatter(Formatter('%(asctime)-s %(module)-15s %(levelname)-8s %(message)s'))        
        handler.stream = sys.stdout
        handler.level  = DEBUG
        self.addHandler(handler)
        self.level     = DEBUG
        self.propagate = False
        
        # Set the logging config file and keep timestamp to compare later
        self.config = _logconf
        self.config_mtime = os.path.getmtime(self.config)        
        
        return ret
        
    
    def _log(self, *args, **kwargs):
        """ Check if we should re-load config file before logging """
        
        # Re-load if config file was modified 
        if os.path.getmtime(self.config) != self.config_mtime:                        
            try:                 
                loadConfig(self.config)
            except Exception:  
                saveConfig(self.config) # Overwrite if failed to load
                self.warning('Cannot load logging configuration %s' % self.config)
                                
            self.config_mtime = os.path.getmtime(self.config)
        
        return Logger._log(self, *args, **kwargs)


class RtLogManager(Manager):
    """ Run-Time Log Manager """
    
    def getLogger(self, name, saveconfig=1):
        """ Save the config whenever we create a new Logger """        

        logexists = int(name in self.loggerDict)
        
        ret = Manager.getLogger(self, name)        
        self.config = ret.config
        
        if saveconfig and not logexists:  
            saveConfig(ret.config)        
        
        return ret



#-------------------------------------------------------------
#  Local functions
#-------------------------------------------------------------

def loadConfig(fname):
    """ Loads a logging configuration from file """
        
    # Read Config file
    cp = ConfigParser()
    cp.read(fname)
    
    handlers   = {}
    formatters = {}    
    lognames   = cp.get('loggers', 'keys').split(',')
    handnames  = cp.get('handlers', 'keys').split(',')
    formnames  = cp.get('formatters', 'keys').split(',')        
    
    # Populate Manager w/ logging configuration (formats, handlers, loggers)
    for key in formnames:
        sec  = 'formatter_' + key        
        formatter = Formatter()
        formatter._fmt    = cp.get(sec, 'format', raw=1)
        formatter.datefmt = cp.get(sec, 'datefmt')
        formatters[key] = formatter
                      
    for key in handnames:
        sec  = 'handler_' + key        
        handler = eval(cp.get(sec, 'class'))()        
        props = dict(cp.items(sec))
        props.pop('class')
        props.pop('level')
        _setHandlerProps(handler, props)
        handler.level     = eval(cp.get(sec, 'level'))
        handler.formatter = formatters[cp.get(sec, 'formatter')]
        handlers[key] = handler
            
    for key in lognames:    
        sec = 'logger_' + key        
        if key in root.manager.loggerDict:
            logger = root.manager.loggerDict[key]
        else:
            logger = root.manager.getLogger(key, saveconfig=0)            
        logger.level     = eval(cp.get(sec, 'level'))
        logger.propagate = eval(cp.get(sec, 'propagate'))
        logger.handlers  = [handlers[h] for h in cp.get(sec, 'handlers').split(',')]
        #logger.channel   = cp.get(sec, 'channel')
        #logger.parent    = cp.get(sec, 'parent')
                   
    
def saveConfig(fname):
    """ Saves logging configuration to enable persistence """
    
    # Retrieve loggers, handlers, formatters
    loggers    = root.manager.loggerDict    
    handlers   = {}
    formatters = {}
    
    for logger in loggers.values():
        for n, handler in enumerate(logger.handlers):
            handlers[logger.name + '.hand%02d' % n] = handler                 
            formatters[logger.name + '.form%02d' % n] = handler.formatter
                  
    # Setup config file by imitating output from logconf.py
    cp = ConfigParser()
 
    cp.add_section('loggers')
    cp.add_section('handlers')
    cp.add_section('formatters')
    
    cp.set('loggers', 'keys', ','.join(sorted(loggers.keys())))
    cp.set('handlers', 'keys', ','.join(sorted(handlers.keys())))
    cp.set('formatters', 'keys', ','.join(sorted(formatters.keys())))
    
    for key, logger in loggers.items():    
        sec  = 'logger_' + key
        cp.add_section(sec)
        cp.set(sec, 'level', _getLevelName(logger.level))
        cp.set(sec, 'propagate', bool(logger.propagate))
        cp.set(sec, 'qualname', logger.name)
        cp.set(sec, 'handlers', ','.join(sorted([k for k, handler in handlers.items() if handler in logger.handlers])))
        cp.set(sec, 'channel', logger.name.split('.')[-1])        
        cp.set(sec, 'parent', (logger.parent.name == 'root') and '(root)' or logger.parent.name)
        
    for key, handler in handlers.items():
        sec  = 'handler_' + key
        cp.add_section(sec)
        cp.set(sec, 'class', str(handler.__class__).split('.')[-1])
        cp.set(sec, 'level', _getLevelName(handler.level))
        cp.set(sec, 'formatter', key.replace('hand', 'form'))        
        for prop, val in _getHandlerProps(handler).items():            
            cp.set(sec, prop, val)
        
    for key, formatter in formatters.items():
        sec  = 'formatter_' + key
        cp.add_section(sec)
        cp.set(sec, 'format', formatter._fmt)
        cp.set(sec, 'datefmt', formatter.datefmt)
                
    # Output to file
    fh = open(fname, 'w')
    cp.write(fh)
    fh.close()


def _getLoggerParent(parent):
    """ Finds parent in list of loggers """
    
    for key, logger in root.manager.loggerDict.items(): 
        if parent == logger:    
            return key
            
    return 'root'


def _getLevelName(val):
    """ Returns Log Level Name from a Log value """

    try:
        return {
           str(NOTSET)   : 'NOTSET',
           str(INFO)     : 'INFO',
           str(DEBUG)    : 'DEBUG',
           str(WARNING)  : 'WARNING',
           str(CRITICAL) : 'CRITICAL',
           str(ERROR)    : 'ERROR',
          }[str(val)]          
    except Exception, e:
        return val


def _getHandlerProps(handler):
    """ Returns dictionary of handler properties and values """
    
    if isinstance(handler, StreamHandler):
        if 'stdout' in str(handler.stream):    
            props = { 'stream' : 'sys.stdout' }
        else:    
            props = { 'stream' : 'sys.stderr' }
            
    elif isinstance(handler, FileHandler):
        props = { 
                    'filename' : handler.baseFilename,
                    'mode'     : handler.mode,
                 }
    else:  
        raise Exception('Handler props unknown for class %s' % handler)
    
    return props

   
def _setHandlerProps(handler, props):
    """ Sets properties for handler """
   
    if isinstance(handler, StreamHandler):
        if props['stream'] == 'sys.stdout':  
            handler.stream = sys.stdout        
        else:                    
            handler.stream = sys.stderr
            
    elif isinstance(handler, FileHandler):
        handler.baseFilename = props['filename']
        handler.mode = props['mode']
        
    else:  
        raise Exception('Handler props unknown for class %s' % handler)
    
    return props

 
#-------------------------------------------------------------
#  Update module attributes that are provided by 'logging' module
#-------------------------------------------------------------
 
def setLoggerConfig(fname):
    _logconf = fname
   
def clearLoggers():
    root.manager.loggerDict.clear()
    saveConfig(_logconf)
   
# Set Loggers to use our RtLogManager Class         
root = RtLogger(WARNING)
Logger.root = root
Logger.manager = RtLogManager(Logger.root)        
   
# Set Loggers to use our RtLogger class
setLoggerClass(RtLogger)

# Load the Config on startup
loadConfig(_logconf)

Discussion

Why would you needs this?

Assume you have a complex system where logging must be minimal to reduce clutter while exhaustive enough to identify issues.

Given the above, it is useful to debug scripts at run-time when your scripts take a long time and/or have a repetitive nature. Most likely, you do not want to kill the script to change log configuration, add print statements, or use a debugger (like pdb) because the script took a long time!! And if the issue occurs only at that state, you might have to re-run the script (a couple times) and expand the logging capture.

If you've done a good job with logging, this script will be helpful for debugging at run-time. I use this method along with a lot of CTRL-Z (suspend) and 'fg' (continue).

Known Issues: None

Alternative Implementations: None

References: Python logging module logconf.py (Use or extend this GUI to modify your configuration)

Sign in to comment