【问题标题】:Custom Logger class and correct line number/function name in log日志中的自定义记录器类和正确的行号/函数名称
【发布时间】:2012-10-10 10:30:19
【问题描述】:

我想将 Python 记录器包装在一个自定义类中,以嵌入一些特定于应用程序的功能并对开发人员隐藏设置细节(设置文件输出、日志记录级别等)。为此,我使用以下 API 创建了一个类:

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

Logger.debug/info/warning/etc 调用通常会在日志中写入进行日志调用的函数和行号。但是,使用我的自定义类,写入日志文件的函数和行号始终相同(对应于自定义类中的 debug()/info()/warning()/error() 函数)。我希望它保存记录 msg 的应用程序代码行。这可能吗?

提前致谢。

【问题讨论】:

  • 也许您可以改为自定义 Handler 或 Formatter 以嵌入特定于应用程序的功能,并提供独立的工厂功能来隐藏设置详细信息?

标签: python logging


【解决方案1】:

是的:sys._getframe(NUM) 其中 NUM 表示您要查找的当前函数之外的函数数量。返回的框架对象具有f_linenof_code.co_filename 等属性。

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

【讨论】:

  • 这些函数非常有用,但是似乎没有办法将这些与 Python 日志记录包集成。因此,我需要选择以下选项之一:(1)根据我的需要自定义 Python 日志包,(2)停止使用 Python 日志包并在我的记录器类中重新实现它的一部分,(3)在里面保留一个 Python 记录器我的类并始终从外部引用它来记录消息(例如 self.mylogger.logger.debug() 或 (4) 删除我的记录器类,直接使用 Python 记录器,为其创建工厂并自定义处理程序/格式化程序(如建议Janne Karila 的评论)。
【解决方案2】:

如果您愿意重新实现一点标准日志记录模块,则可以生成日志包装器。诀窍是编写自己的 findCaller() 方法,该方法知道在解释回溯时如何忽略日志记录包装器源文件。

在 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

还有一个使用它的例子:

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

【讨论】:

    【解决方案3】:

    这是重写findCaller 的又一次尝试。这使您可以根据每个函数自定义额外的堆栈帧深度。

    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```
    

    【讨论】:

      【解决方案4】:

      基于@Will Ware 的回答。另一种选择是覆盖findCaller 方法并使用自定义类作为默认记录器:

      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
      

      【讨论】:

        【解决方案5】:

        更改 lineno 和文件名的最佳位置(如果您真的想这样做)是在附加到记录器的过滤器中。这是一个概念验证。最后一行将记录一条带有文件名和最后一行的自定义消息:

        import logging
        import inspect
        from pathlib import Path
        
        class up_stacked_logger:
            def __init__(self, logger, n):
                self.logger = logger
        
                calling_frame = inspect.stack()[n+1].frame
                trace = inspect.getframeinfo(calling_frame)  
        
                class UpStackFilter(logging.Filter):
                    def filter(self, record):       
                        record.lineno = trace.lineno
                        record.pathname = trace.filename
                        record.filename = Path(trace.filename).name
                        return True
        
                self.f = UpStackFilter()
        
            def __enter__(self):
                self.logger.addFilter(self.f)
                return self.logger
        
            def __exit__(self, *args, **kwds):
                self.logger.removeFilter(self.f)
        
        
        def my_cool_customized_log_function(logger, msg):  
        
            with up_stacked_logger(logger, n=1) as logger:
                logger.info(f'--> customize {msg}')
        
        
        logging.basicConfig(level=logging.DEBUG, format="[%(name)s][%(levelname)s]  %(message)s (%(filename)s:%(lineno)d)")
        logger = logging.getLogger('some.module.logger')  
        
        my_cool_customized_log_function(logger, 'a message')
        

        【讨论】:

          【解决方案6】:

          试试stacklevel,它计算从原始日志调用到记录器的debug()info()等调用的次数。这是logging 3.8 中的新功能:

          第三个可选关键字参数是stacklevel,默认为 1.如果大于1,则在计算中设置的行号和函数名时跳过对应的栈帧数 为日志记录事件创建的 LogRecord。这可以用于记录 帮助器,以便记录函数名、文件名和行号 不是辅助函数/方法的信息,而是它的 呼叫者。此参数的名称反映了 警告模块。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2015-06-09
            • 1970-01-01
            • 2022-09-23
            • 1970-01-01
            • 2018-12-08
            • 2020-12-08
            • 2016-04-23
            相关资源
            最近更新 更多