Custom Logger class and correct line number/function name in log
It is possible to generate a log wrapper if you are willing to re-implent a little bit of the standard logging module. The trick is to write your own findCaller() method that knows how to ignore your logging-wrapper source-file when interpreting back-traces.
in logwrapper.py:
import logging
import os
import sys
from logging import *
# This code is mainly copied from the python logging module, with minor modifications
# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame.
#
if hasattr(sys, 'frozen'): #support for py2exe
_srcfile = "logging%s__init__%s" % (os.sep, __file__[-4:])
elif __file__[-4:].lower() in ['.pyc', '.pyo']:
_srcfile = __file__[:-4] + '.py'
else:
_srcfile = __file__
_srcfile = os.path.normcase(_srcfile)
class LogWrapper(object):
def __init__(self, logger):
self.logger = logger
def debug(self, msg, *args, **kwargs):
"""
Log 'msg % args' with severity 'DEBUG'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
"""
if self.logger.isEnabledFor(DEBUG):
self._log(DEBUG, msg, args, **kwargs)
def info(self, msg, *args, **kwargs):
"""
Log 'msg % args' with severity 'INFO'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.info("Houston, we have a %s", "interesting problem", exc_info=1)
"""
if self.logger.isEnabledFor(INFO):
self._log(INFO, msg, args, **kwargs)
# Add other convenience methods
def log(self, level, msg, *args, **kwargs):
"""
Log 'msg % args' with the integer severity 'level'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.log(level, "We have a %s", "mysterious problem", exc_info=1)
"""
if not isinstance(level, int):
if logging.raiseExceptions:
raise TypeError("level must be an integer")
else:
return
if self.logger.isEnabledFor(level):
self._log(level, msg, args, **kwargs)
def _log(self, level, msg, args, exc_info=None, extra=None):
"""
Low-level logging routine which creates a LogRecord and then calls
all the handlers of this logger to handle the record.
"""
# Add wrapping functionality here.
if _srcfile:
#IronPython doesn't track Python frames, so findCaller throws an
#exception on some versions of IronPython. We trap it here so that
#IronPython can use logging.
try:
fn, lno, func = self.findCaller()
except ValueError:
fn, lno, func = "(unknown file)", 0, "(unknown function)"
else:
fn, lno, func = "(unknown file)", 0, "(unknown function)"
if exc_info:
if not isinstance(exc_info, tuple):
exc_info = sys.exc_info()
record = self.logger.makeRecord(
self.logger.name, level, fn, lno, msg, args, exc_info, func, extra)
self.logger.handle(record)
def findCaller(self):
"""
Find the stack frame of the caller so that we can note the source
file name, line number and function name.
"""
f = logging.currentframe()
#On some versions of IronPython, currentframe() returns None if
#IronPython isn't run with -X:Frames.
if f is not None:
f = f.f_back
rv = "(unknown file)", 0, "(unknown function)"
while hasattr(f, "f_code"):
co = f.f_code
filename = os.path.normcase(co.co_filename)
if filename == _srcfile:
f = f.f_back
continue
rv = (co.co_filename, f.f_lineno, co.co_name)
break
return rv
And an example of using it:
import logging
from logwrapper import LogWrapper
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(filename)s(%(lineno)d): "
"%(message)s")
logger = logging.getLogger()
lw = LogWrapper(logger)
lw.info('Wrapped well, this is interesting')
Yes: sys._getframe(NUM)
where NUM says how how many functions outside the current one you are looking for. The returned frame object has attributes like f_lineno
and f_code.co_filename
.
http://docs.python.org/library/sys.html#sys._getframe
Based on @Will Ware's answer. Another option is to overwrite findCaller
method and use custom class as default logger:
class MyLogger(logging.Logger):
"""
Needs to produce correct line numbers
"""
def findCaller(self):
n_frames_upper = 2
f = logging.currentframe()
for _ in range(2 + n_frames_upper): # <-- correct frame
if f is not None:
f = f.f_back
rv = "(unknown file)", 0, "(unknown function)"
while hasattr(f, "f_code"):
co = f.f_code
filename = os.path.normcase(co.co_filename)
if filename == logging._srcfile:
f = f.f_back
continue
rv = (co.co_filename, f.f_lineno, co.co_name)
break
return rv
logging.setLoggerClass(MyLogger)
logger = logging.getLogger('MyLogger') # init MyLogger
logging.setLoggerClass(logging.Logger) # reset logger class to default