mirror of
https://github.com/rzzzwilson/pymlac.git
synced 2025-06-10 09:32:41 +00:00
213 lines
5.9 KiB
Python
213 lines
5.9 KiB
Python
#!/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 != <this module name>
|
|
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()
|
|
|