14

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

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

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

提前致谢。

4

6 回答 6

6

如果您愿意重新实现一点标准日志记录模块,则可以生成日志包装器。诀窍是编写自己的 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')
于 2014-02-28T09:33:36.703 回答
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
于 2018-11-09T12:21:56.277 回答
2

是的:sys._getframe(NUM)NUM 表示您要查找的当前功能之外的功能数量。返回的框架对象具有 和 之类的f_lineno属性f_code.co_filename

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

于 2012-10-19T19:10:27.497 回答
1

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

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

于 2019-12-26T18:53:45.230 回答
0

这是重写的又一次尝试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```
于 2017-11-10T03:23:52.317 回答
0

更改 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')
于 2019-08-22T18:55:27.303 回答