#!/usr/bin/env python """ A simple logger. Simple usage: import log log = log.Log('my_log.log', log.Log.DEBUG) log('A line in the log at the default level (DEBUG)') log('A log line at WARN level', Log.WARN) log.info('log line issued at INFO level') Based on the 'borg' recipe from [http://code.activestate.com/recipes/66531/]. Log levels styled on the Python 'logging' module. Log output includes the module and line # of the log() call. """ import os import sys import datetime import traceback ################################################################################ # A simple (?) logger. ################################################################################ class Log(object): __shared_state = {} # this __dict__ shared by ALL instances # the predefined logging levels CRITICAL = 50 ERROR = 40 WARN = 30 INFO = 20 DEBUG = 10 NOTSET = 0 _level_num_to_name = {NOTSET: 'NOTSET', DEBUG: 'DEBUG', INFO: 'INFO', WARN: 'WARN', ERROR: 'ERROR', CRITICAL: 'CRITICAL'} # default maximum length of filename (enforced) DefaultMaxFname = 15 def __init__(self, logfile=None, level=NOTSET, append=False, max_fname=DefaultMaxFname): """Initialise the logging object. logfile the path to the log file level logging level - don't log below this level append True if log file is appended to """ # make sure we have same state as all other log objects self.__dict__ = Log.__shared_state self.max_fname = max_fname self.sym_level = 'NOTSET' # set in call to check_level() self.level = self.check_level(level) # don't allow logfile to change after initially set if not hasattr(self, 'logfile'): if logfile is None: logfile = '%s.log' % __name__ try: if append: self.logfd = open(logfile, 'a') else: self.logfd = open(logfile, 'w') except IOError: # assume we have readonly filesystem basefile = os.path.basename(logfile) if sys.platform == 'win32': logfile = os.path.join('C:\\', basefile) else: logfile = os.path.join('~', basefile) # try to open logfile again if append: self.logfd = open(logfile, 'a') else: self.logfd = open(logfile, 'w') self.logfile = logfile self.critical('='*55) self.critical('Log started on %s, log level=%s' % (datetime.datetime.now().ctime(), self._level_num_to_name[level])) self.critical('-'*55) def check_level(self, level): """Check the level value for legality. If 'level' is invalid, raise Exception. If valid, return value. """ try: level = int(level) except ValueError: msg = "Logging level invalid: '%s'" % str(level) print(msg) raise Exception(msg) if not 0 <= level <= 50: msg = "Logging level invalid: '%s'" % str(level) print(msg) raise Exception(msg) return level def set_level(self, level): """Set logging level.""" level = self.check_level(level) # convert numeric level to symbolic sym = self._level_num_to_name.get(level, None) if sym is None: # not recognized symbolic but it's legal, so interpret as 'XXXX+2' sym_10 = 10 * (level/10) sym_rem = level - sym_10 sym = '%s+%d' % (self._level_num_to_name[sym_10], sym_rem) self.level = level self.sym_level = sym self.critical('Logging level set to %02d (%s)' % (level, sym)) def __call__(self, msg=None, level=None): """Call on the logging object. msg message string to log level level to log 'msg' at (if not given, assume self.level) """ # get level to log at if level is None: level = self.level # are we going to log? if level < self.level: return if msg is None: msg = '' # get time to = datetime.datetime.now() hr = to.hour min = to.minute sec = to.second msec = to.microsecond # caller information - look back for first module != frames = traceback.extract_stack() frames.reverse() try: (_, mod_name) = __name__.rsplit('.', 1) except ValueError: mod_name = __name__ for (fpath, lnum, mname, _) in frames: fname = os.path.basename(fpath).rsplit('.', 1)[0] if fname != mod_name: break # get string for log level loglevel = self._level_num_to_name[level] fname = fname[:self.max_fname] self.logfd.write('%02d:%02d:%02d.%06d|%8s|%*s:%-4d|%s\n' % (hr, min, sec, msec, loglevel, self.max_fname, fname, lnum, msg)) self.logfd.flush() def critical(self, msg): """Log a message at CRITICAL level.""" self(msg, self.CRITICAL) def error(self, msg): """Log a message at ERROR level.""" self(msg, self.ERROR) def warn(self, msg): """Log a message at WARN level.""" self(msg, self.WARN) def info(self, msg): """Log a message at INFO level.""" self(msg, self.INFO) def debug(self, msg): """Log a message at DEBUG level.""" self(msg, self.DEBUG) def __del__(self): self.logfd.close()