problems with logging
Steven Armstrong <[email protected]>
| Newsgroups | gmane.comp.web.pyblosxom.devel |
|---|---|
| Message-ID | <[email protected]> |
Hi all
While cleaning up the wsgi code I bumped into a few problems with the
current logging functionality.
Using the tools.make_logger("/path/to/logfile") it is not possible to
log to different logfiles.
Every new call to tools.make_logger hijackes all log messages from
previous callers.
For example the comments plugin logs to a file called
/<logdir>/comments.log. The xmlrpc_metaweblog plugin to a file named
/<logdir>/metaweblog.log. All messages logged by comments are logged to
metaweblog.log. Depending on who called tools.make_logger last.
Here's a patch that changes this behaviour. It wasn't possible to
correct this with the old API. So I changed it, back-combat offcourse.
Old:
tools.make_logger("/path/to/logfile")
tools.log("whatever")
New:
log = tools.getLogger(filename="/path/to/logfile")
log.info("some info")
log.error("some error")
log.setLevel(log.DEBUG)
log.debug("some debug message")
# or:
log = tools.getLogger("logname", "debug")
# this logs to /<logdir>/logname.log setting the default loglevel to DEBUG.
# or: backwards compatible
# note: this still hijackes log messages.
tools.make_logger("/path/to/logfile")
tools.log("whatever")
I added a initialize and a cleanup function to the tools.py module.
They are called from PyBlosxom.initialize resp. PyBlosxom.cleanup.
These calls are used to set initial logdir resp. to close open logfiles
when done.
The patch changes the following files:
/pyblosxom/Pyblosxom/tools.py
/pyblosxom/Pyblosxom/pyblosxom.py
Do you see any problems with this?
cheers
Steven
ps: I'm polishing the wsgi code atm. I'll send in a patch within a few
hours.
logging.diff
(text/plain, 13 KB)
Index: Pyblosxom/pyblosxom.py
===================================================================
RCS file: /cvsroot/pyblosxom/pyblosxom/Pyblosxom/pyblosxom.py,v
retrieving revision 1.55
diff -u -r1.55 pyblosxom.py
--- Pyblosxom/pyblosxom.py 1 Feb 2005 01:07:26 -0000 1.55
+++ Pyblosxom/pyblosxom.py 4 Feb 2005 16:06:14 -0000
@@ -56,6 +56,9 @@
pyhttp = self._request.getHttp()
config = self._request.getConfiguration()
+ # initialize the tools module
+ tools.initialize(config)
+
data["pyblosxom_version"] = VERSION_DATE
data['pi_bl'] = ''
@@ -83,6 +86,15 @@
mappingfunc=lambda x,y:y,
defaultfunc=lambda x:x)
+ def cleanup(self):
+ """
+ Cleanup everything.
+ This should be called when Pyblosxom has done all its work.
+ Right before exiting.
+ """
+ tools.cleanup()
+
+
def getRequest(self):
"""
Returns the L{Request} object.
@@ -129,6 +141,8 @@
# do end callback
tools.run_callback("end", {'request': self._request})
+
+ self.cleanup()
def runCallback(self, callback="help"):
@@ -298,6 +312,8 @@
print "rendering '%s' ..." % url
tools.render_url(config, url, q)
+ self.cleanup()
+
def testInstallation(self):
test_installation(self._request)
Index: Pyblosxom/tools.py
===================================================================
RCS file: /cvsroot/pyblosxom/pyblosxom/Pyblosxom/tools.py,v
retrieving revision 1.39
diff -u -r1.39 tools.py
--- Pyblosxom/tools.py 1 Feb 2005 01:07:26 -0000 1.39
+++ Pyblosxom/tools.py 4 Feb 2005 16:06:16 -0000
@@ -8,9 +8,14 @@
@var num2month: A dict of number month format to its literal format
@var MONTHS: A list of valid literal and numeral months
@var VAR_REGEXP: Regular expression for detection and substituion of variables
+@var _logger_registry: A dict of created loggers. Prevents multiple loggers/handlers using the same file.
+@var _logdir: The directory in which logfiles are created by default
+@var _config: A reference to the pyblosxom config dict.
"""
import plugin_utils
-import sgmllib, re, os, string, types, time, os.path, StringIO, sys
+import sgmllib, re, os, string, types, time, os.path, sys
+try: from cStringIO import StringIO
+except ImportError: from StringIO import StringIO
month2num = { 'nil' : '00',
'Jan' : '01',
@@ -39,6 +44,33 @@
# regular expression for detection and substituion of variables.
VAR_REGEXP = re.compile(ur'(?<!\\)\$((?:\w|\-|::\w)+(?:\(.*?(?<!\\)\))?)')
+# see module docstring for infos
+_logger_registry = {}
+_logdir = "/tmp"
+_config = None
+
+def initialize(config):
+ """
+ Initialize the tools module.
+ This gives the module a chance to use the pyblosxom config.py file
+ without having to change sys.path and importing it manually.
+ This should be called from Pyblosxom.pyblosxom.PyBlosxom.initialize.
+ """
+ global _logdir, _config
+ _logdir = config.get('logdir', _logdir)
+ _config = config
+
+def cleanup():
+ """
+ Cleanup the tools module.
+ This should be called from Pyblosxom.pyblosxom.PyBlosxom.cleanup.
+ """
+ # close log files
+ for l in _logger_registry:
+ if hasattr(_logger_registry[l], 'cleanup'):
+ _logger_registry[l].cleanup()
+
+
class Stripper(sgmllib.SGMLParser):
"""
Strips HTML
@@ -488,51 +520,6 @@
return mycache
-_logger_registry = {}
-def make_logger(filename):
- """
- Create a logging function called log, which logs to the supplied filename
- usage is:
-
- -->>> tools.make_logger('/tmp/pybloxom.log')
- -->>> tools.log('log message')
-
- @param filename: the name of a file to log to
- @type filename: string
- """
- global log
- try:
- import logging
- except ImportError:
- def log(*args):
- f = open(filename, "a")
- for i in args:
- f.write("%s INFO %s" % (time.asctime(), repr(i)))
- f.write("\n")
- f.close()
- else:
- global _logger_registry
- # if all loggers have the same name,
- # everything is logged to all files.
- logger_name = os.path.splitext(os.path.basename(filename))[0]
- # only add one handler per logger
- if not logger_name in _logger_registry:
- _logger = logging.getLogger(logger_name)
- hdlr = logging.FileHandler(filename)
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
- hdlr.setFormatter(formatter)
- _logger.addHandler(hdlr)
- _logger.setLevel(logging.INFO)
- _logger_registry[logger_name] = _logger
-
- logger = _logger_registry[logger_name]
-
- def log(*args):
- # adjusted to match the 'manual' log func
- for i in args:
- logger.info(repr(i))
-
-
def update_static_entry(cdict, entry_filename):
"""
This is a utility function that allows plugins to easily update
@@ -620,6 +607,262 @@
f.write(response.read())
f.close()
+
+#******************************
+# Logging
+#******************************
+
+class _Logger:
+ """
+ Emulates some functionality of the logging module for older Python installations.
+ """
+ CRITICAL = 50
+ FATAL = CRITICAL
+ ERROR = 40
+ WARNING = 30
+ WARN = WARNING
+ INFO = 20
+ DEBUG = 10
+ NOTSET = 0
+
+ # order matters!
+ _names = ['FATAL', 'CRITICAL', 'ERROR', 'WARN', 'WARNING', 'INFO', 'DEBUG', 'NOTSET']
+
+ # populated in __init__
+ _levels = {}
+ _level_names = {}
+
+ def __init__(self, filename, level):
+ self._populate_levels()
+ self.filename = filename
+ self.setLevel(level)
+ self.format = "%Y-%m-%d %H:%M:%S"
+ self._file = None
+
+ def _populate_levels(self):
+ for n in self._names:
+ self._levels[n] = getattr(self, n)
+ self._level_names[getattr(self, n)] = n
+
+ def _getLevel(self, level):
+ """
+ Return the numeric representation of logging level 'level'.
+ Defaults to 0 (self.NOTSET).
+ """
+ return self._levels.get(level.upper(), self.NOTSET)
+
+ def _getMessage(self, msg, args):
+ """
+ Return the message for this log record after merging any user-supplied
+ arguments with the message.
+ """
+ import types
+ if not hasattr(types, "UnicodeType"): #if no unicode support...
+ msg = str(msg)
+ else:
+ try:
+ msg = str(msg)
+ except UnicodeError:
+ pass
+ if args:
+ msg = msg % args
+ return msg
+
+ def _getExcInfo(self):
+ import traceback
+ (exc_type, exc_value, tb) = sys.exc_info()
+ exc_file = StringIO()
+ traceback.print_exception(exc_type, exc_value, tb, file=exc_file)
+ exc_string = exc_file.getvalue()
+ return exc_string
+
+ def _log(self, level, msg, args, exc_info=None):
+ if self.level <= level:
+ ct = time.time()
+ msecs = (ct - long(ct)) * 1000
+ time_string = "%s,%03d" % (time.strftime(self.format, time.localtime(ct)), msecs)
+ level = self.getLevelName(level)
+ msg = self._getMessage(msg, args)
+
+ if not self._file:
+ self._file = open(self.filename, "a")
+ f.write("%s %s %s\n" % (time_string, level, msg))
+ if exc_info: f.write(self._getExcInfo())
+ #f.close()
+
+ def cleanup(self):
+ """
+ Closes open log files.
+ """
+ if self._file:
+ self._file.close()
+
+ # public methods
+
+ def getLevelName(self, level):
+ """
+ Return the textual representation of logging level 'level'.
+
+ If the level is one of the predefined levels (CRITICAL, ERROR, WARNING,
+ INFO, DEBUG) then you get the corresponding string.
+ Otherwise, the string "Level %s" % level is returned.
+ """
+ return self._level_names.get(level, ("Level %s" % level))
+
+ def setLevel(self, level):
+ if isinstance(level, int):
+ self.level = level
+ else:
+ self.level = self._getLevel(level.upper())
+
+ def critical(self, msg, *args, **kwargs):
+ apply(self._log, (self.CRITICAL, msg, args), kwargs)
+ fatal = critical
+
+ def error(self, msg, *args, **kwargs):
+ apply(self._log, (self.ERROR, msg, args), kwargs)
+
+ def warning(self, msg, *args, **kwargs):
+ apply(self._log, (self.WARNING, msg, args), kwargs)
+ warn = warning
+
+ def info(self, msg, *args, **kwargs):
+ apply(self._log, (self.INFO, msg, args), kwargs)
+
+ def debug(self, msg, *args, **kwargs):
+ apply(self._log, (self.DEBUG, msg, args), kwargs)
+
+def getLogger(logname="pyblosxom", level="info", filename=None):
+ """
+ log = tools.getLogger("logname", "info")
+ # this creates a logger that logs to 'logdir'/logname.log
+ log.info("We %s a %s", "have", "good time")
+ log.debug("We %s a %s", "are testing", "new function")
+ log.setLevel(log.ERROR)
+ log.error("We %s a %s", "have", "mysterious problem", exc_info=True)
+
+ or:
+ log = tools.getLogger(filename="/path/to/logfile", level="info")
+
+ @param logname: the name of this logger. used to figure out the logfile
+ @type logname: string
+
+ @param level: the default loglevel
+ @type level: string
+
+ @param filename: the file to log to
+ @type filename: string
+ """
+ if filename:
+ logname = os.path.splitext(os.path.basename(filename))[0]
+ else:
+ filename = "%s%s%s.log" % (_logdir, os.sep, logname)
+ filename = os.path.normpath(filename)
+
+ global _logger_registry
+ try:
+ import logging
+ #raise ImportError, "whatever"
+ if not filename in _logger_registry:
+ int_level = getattr(logging, level.upper())
+ _logger = logging.getLogger(logname)
+ hdlr = logging.FileHandler(filename)
+ hdlr.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
+ _logger.addHandler(hdlr)
+ _logger.setLevel(int_level)
+ for l in ['CRITICAL', 'FATAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'getLevelName']:
+ setattr(_logger, l, getattr(logging, l))
+ _logger_registry[filename] = _logger
+ except ImportError:
+ if not filename in _logger_registry:
+ _logger_registry[filename] = _Logger(filename, level)
+
+ # update loglevel if necessary
+ logger = _logger_registry[filename]
+ new_level = getattr(logger, level.upper())
+ if new_level < logger.level:
+ logger.setLevel(new_level)
+ return logger
+
+def log_exception(logname="error", level="error", filename=None):
+ """
+ Logs a exception. By default to a file named error.log in
+ your 'logdir' directory. Uses /tmp if 'logdir' is not set.
+ You can also pass your own logfile. See below.
+
+ Usage:
+ try:
+ whatever
+ except:
+ import tools
+ tools.log_exception()
+ # or:
+ #tools.log_exception(filename="/path/to/logfile")
+
+ @param logname: the name of this logger. used to figure out the logfile
+ @type logname: string
+
+ @param level: the default loglevel
+ @type level: string
+
+ @param filename: the file to log to
+ @type filename: string
+ """
+ log = getLogger(logname, level, filename)
+ log.error("Exception occured:", exc_info=True)
+
+def log_frame(_log, num):
+ """
+ Logs some info about the calling function/method.
+ Usefull for debugging.
+ Usage:
+ import tools
+ log = tools.getLogger(filename="/path/to/logfile")
+ tool.log_frame(log, 3)
+ tool.log_frame(log, 2)
+ tool.log_frame(log, 1)
+
+ @param _log: instance of a logger
+ @type _log: L{_Logger} or logging module logger
+
+ @param num: index of the frame
+ @type num: int
+ """
+ f = sys._getframe(num)
+ module = f.f_globals["__name__"]
+ filename = f.f_code.co_filename
+ line = f.f_lineno
+ subr = f.f_code.co_name
+ _log.info("module: %s\nfilename: %s\nline: %s\nsubroutine: %s",
+ module, filename, line, subr)
+
+
+def make_logger(filename):
+ """
+ DEPRECATED
+ Use this instead:
+ -->>> log = tools.getLogger(filename='/tmp/pybloxom.log')
+ -->>> log.info('log message')
+ -->>> log.error('the programm made %s', 'a bubu')
+ ----------
+
+ Create a logging function called log, which logs to the supplied filename
+ usage is:
+
+ -->>> tools.make_logger('/tmp/pybloxom.log')
+ -->>> tools.log('log message')
+
+ @param filename: the name of a file to log to
+ @type filename: string
+ """
+ global log
+
+ _logger = getLogger(filename=filename)
+
+ def log(*args):
+ for i in args:
+ _logger.info(repr(i))
+
# %<-------------------------
# BEGIN portalocking block from Python Cookbook.