Clase de registrador personalizada y número de línea / nombre de función correctos en el registro

Me gustaría incluir Python logger en una clase personalizada para incrustar alguna funcionalidad específica de la aplicación y ocultar los detalles de configuración de los desarrolladores (configuración de salida de archivo, nivel de registro, etc.) Para hacer esto, creé una clase con la siguiente API:

__init__(log_level, filename) debug(msg) info(msg) warning(msg) error(msg) 

Las llamadas Logger.debug / info / warning / etc generalmente escriben en el registro la función y el número de línea donde se realizó la llamada de registro. Sin embargo, al usar mi clase personalizada, la función y los números de línea escritos en el archivo de registro son siempre los mismos (correspondientes a las funciones debug () / info () / warning () / error () dentro de la clase personalizada). Quiero que guarde la línea de código de la aplicación que registró el msg. ¿Es eso posible?

Gracias por adelantado.

Sí: sys._getframe(NUM) donde NUM dice cuántas funciones fuera de la actual está buscando. El objeto de marco devuelto tiene atributos como f_lineno y f_code.co_filename .

http://docs.python.org/library/sys.html#sys._getframe

Es posible generar un contenedor de registro si está dispuesto a volver a implementar un poco del módulo de registro estándar. El truco consiste en escribir su propio método findCaller () que sepa cómo ignorar el archivo de origen de contenedor de registro al interpretar las trazas de fondo.

en 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, eg 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, eg 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, eg 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 

Y un ejemplo de usarlo:

 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') 

Basado en la respuesta de @Will Ware. Otra opción es sobrescribir el método findCaller y usar la clase personalizada como registrador predeterminado:

 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 

Aquí hay otro findCaller volver a escribir findCaller . Esto le permite personalizar la profundidad de marco de stack adicional por función.

 import os import logging from contextlib import contextmanager logging.basicConfig( format='%(asctime)-15s %(levelname)s %(filename)s:%(lineno)d %(message)s', level=logging.INFO ) @contextmanager def logdelta(n, level=logging.DEBUG): _frame_stuff = [0, logging.Logger.findCaller] def findCaller(_): f = logging.currentframe() for _ in range(2 + _frame_stuff[0]): 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 rootLogger = logging.getLogger() isEnabled = rootLogger.isEnabledFor(level) d = _frame_stuff[0] try: logging.Logger.findCaller = findCaller _frame_stuff[0] = d + n yield isEnabled except: raise finally: logging.Logger.findCaller = _frame_stuff[1] _frame_stuff[0] = d def A(x): with logdelta(1): logging.info('A: ' + x) # Don't log with this line number def B(x): with logdelta(2): logging.info('A: ' + x) # or with this line number def C(x): B(x) # or this line number A('hello') # Instead, log with THIS line number C('hello') # or THIS line number```