32

这是我用于记录的格式化字符串:

'%(asctime)s - %(levelname)-10s - %(funcName)s - %(message)s'

但是为了显示日志消息,我有一个包装器做更多的事情(我设置了不同的日志级别,配置不同的日志后端,提供方便的功能来访问自定义级别等):

class MyLogger(logging.Logger):

    def split_line(self, level, message):
        ....
        self.log.(level, line)

    def progress(self, message):
        self.split_line(PROGRESS, message)

使用此设置,每当我记录某些内容时:

def myfunc():
    log.progress('Hello')

我得到:

013-10-27 08:47:30,130 - PROGRESS   - split_line - Hello

这不是我想要的,即:

013-10-27 08:47:30,130 - PROGRESS   - myfunc     - Hello

如何告诉记录器为函数名称使用正确的上下文?我认为这实际上会比堆栈帧高两个级别。

编辑

这是一个显示问题的测试程序:

import sys
import logging

PROGRESS = 1000

class MyLogger(logging.Logger):

    PROGRESS = PROGRESS
    LOG_FORMATTER = '%(asctime)s - %(levelname)-10s - %(funcName)s - %(message)s'
    DEF_LOGGING_LEVEL = logging.WARNING

    def __init__(self, log_name, level=None):
        logging.Logger.__init__(self, log_name)
        self.formatter = logging.Formatter(self.LOG_FORMATTER)
        self.initLogger(level)

    def initLogger(self, level=None):
        self.setLevel(level or self.DEF_LOGGING_LEVEL)
        self.propagate = False

    def add_handler(self, log_file, use_syslog):
        if use_syslog : hdlr = logging.handlers.SysLogHandler(address='/dev/log')
        elif log_file : hdlr = logging.FileHandler(log_file)
        else          : hdlr = logging.StreamHandler(sys.stderr)
        hdlr.setFormatter(self.formatter)
        self.addHandler(hdlr)
        return hdlr

    def addHandlers(self, log_file=None, progress_file=None, use_syslog=False):
        self.logger_hdlr = self.add_handler(log_file, use_syslog)
        if progress_file:
            self.progress_hdlr = self.add_handler(progress_file, use_syslog)
            self.progress_hdlr.setLevel(self.PROGRESS)
        else:
            self.progress_hdlr = None

    def split_line(self, level, txt, *args):
        txt = txt % (args)
        for line in txt.split('\n'):
            self.log(level, line)

    def progress(self, txt, *args):
        self.split_line(self.PROGRESS, txt, *args)

logging.setLoggerClass(MyLogger)
logging.addLevelName(PROGRESS, 'PROGRESS')
logger = logging.getLogger(__name__)
logger.addHandlers()

name = 'John'
logger.progress('Hello %s\nHow are you doing?', name)

产生:

2013-10-27 09:47:39,577 - PROGRESS   - split_line - Hello John
2013-10-27 09:47:39,577 - PROGRESS   - split_line - How are you doing?
4

9 回答 9

4

本质上,应该归咎于Logger类的代码:

这种方法

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

返回调用者链中不属于当前模块的第一个函数。

您可以Logger通过添加稍微复杂的逻辑来继承和覆盖此方法。跳过另一个级别的调用深度或添加另一个条件。


在您非常特殊的情况下,避免自动行拆分并执行以下操作可能会更简单

logger.progress('Hello %s', name)
logger.progress('How are you doing?')

或做

def splitter(txt, *args)
    txt = txt % (args)
    for line in txt.split('\n'):
        yield line

for line in splitter('Hello %s\nHow are you doing?', name):
    logger.progress(line)

并有一个

def progress(self, txt, *args):
    self.log(self.PROGRESS, txt, *args)

可能它会为您省去很多麻烦。

编辑2:不,这无济于事。它现在会将您显示progress为您的调用者函数名称...

于 2013-10-27T09:11:33.263 回答
3

首先,根据您的代码,很清楚为什么会发生这种情况,levelname并且当您调用is和is时funcName“属于” so 。self.logself.log(level, line)levelnamelevelfuncNameline

您有 2 个选项恕我直言:

  1. 使用inspect模块获取当前方法并将其传递到消息中,然后您可以对其进行解析并非常轻松地使用它。

  2. 更好的方法是inspect在 split_line 内部使用来获取“父亲”方法,您可以将以下代码中的 number(3) 更改为“播放”方法的层次结构。

使用检查获取当前方法的示例

from inspect import stack

class Foo:
    def __init__(self):
        print stack()[0][3]

f = Foo()
于 2013-10-27T08:51:05.807 回答
3

感谢@cygnusb 和其他已经提供有用指针的人。我选择使用 Python 3.4 Logger.findCaller 方法作为我的起点。以下解决方案已使用 Python 2.7.9 和 3.4.2 进行了测试。此代码旨在放置在其自己的模块中。它只用循环的一次迭代就产生了正确的答案。

import io
import sys

def _DummyFn(*args, **kwargs):
    """Placeholder function.

    Raises:
        NotImplementedError
    """
    _, _ = args, kwargs
    raise NotImplementedError()

# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame, by skipping frames whose filename is that of this
# module's source. It therefore should contain the filename of this module's
# source file.
_srcfile = os.path.normcase(_DummyFn.__code__.co_filename)
if hasattr(sys, '_getframe'):
    def currentframe():
        return sys._getframe(3)
else:  # pragma: no cover
    def currentframe():
        """Return the frame object for the caller's stack frame."""
        try:
            raise Exception
        except Exception:
            return sys.exc_info()[2].tb_frame.f_back

class WrappedLogger(logging.Logger):
    """Report context of the caller of the function that issues a logging call.

    That is, if

        A() -> B() -> logging.info()

    Then references to "%(funcName)s", for example, will use A's context
    rather than B's context.

    Usage:
        logging.setLoggerClass(WrappedLogger)
        wrapped_logging = logging.getLogger("wrapped_logging")
    """
    def findCaller(self, stack_info=False):
        """Return the context of the caller's parent.

        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.

        This is based on the standard python 3.4 Logger.findCaller method.
        """
        sinfo = None
        f = 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

        if sys.version_info.major == 2:
            rv = "(unknown file)", 0, "(unknown function)"
        else:
            rv = "(unknown file)", 0, "(unknown function)", sinfo

        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == _srcfile or filename == logging._srcfile:
                f = f.f_back
                continue
            # We want the frame of the caller of the wrapped logging function.
            # So jump back one more frame.
            f = f.f_back
            co = f.f_code
            if sys.version_info.major == 2:
            rv = "(unknown file)", 0, "(unknown function)"
        else:
            rv = "(unknown file)", 0, "(unknown function)", sinfo

        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == _srcfile or filename == logging._srcfile:
                f = f.f_back
                continue
            # We want the frame of the caller of the wrapped logging function.
            # So jump back one more frame.
            f = f.f_back
            co = f.f_code
            if sys.version_info.major == 2:
                rv = co.co_filename, f.f_lineno, co.co_name
            else:
                if stack_info:
                    sio = io.StringIO()
                    sio.write('Stack (most recent call last):\n')
                    traceback.print_stack(f, file=sio)
                    sinfo = sio.getvalue()
                    if sinfo[-1] == '\n':
                        sinfo = sinfo[:-1]
                    sio.close()
                rv = co.co_filename, f.f_lineno, co.co_name, sinfo
            break

        return rv
于 2015-02-06T01:59:35.797 回答
3

这在Python 3.8中通过添加 stacklevel 参数得到修复。但是,我从 cpython中获取了 findCaller 的当前实现来制作 Python 3.7 兼容版本。

综合以上答案:

import sys,os

#Get both logger's and this file's path so the wrapped logger can tell when its looking at the code stack outside of this file.
_loggingfile = os.path.normcase(logging.__file__)
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)
_wrongCallerFiles = set([_loggingfile, _srcfile])

#Subclass the original logger and overwrite findCaller
class WrappedLogger(logging.Logger):
    def __init__(self, name):
        logging.Logger.__init__(self, name)

    #Modified slightly from cpython's implementation https://github.com/python/cpython/blob/master/Lib/logging/__init__.py#L1374
    def findCaller(self, stack_info=False, stacklevel=1):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.
        """
        f = 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
        orig_f = f
        while f and stacklevel > 1:
            f = f.f_back
            stacklevel -= 1
        if not f:
            f = orig_f
        rv = "(unknown file)", 0, "(unknown function)", None
        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename in _wrongCallerFiles:
                f = f.f_back
                continue
            sinfo = None
            if stack_info:
                sio = io.StringIO()
                sio.write('Stack (most recent call last):\n')
                traceback.print_stack(f, file=sio)
                sinfo = sio.getvalue()
                if sinfo[-1] == '\n':
                sinfo = sinfo[:-1]
            sio.close()
        rv = (co.co_filename, f.f_lineno, co.co_name, sinfo)
        break
    return rv
于 2019-10-24T01:36:02.070 回答
2

您可以合并progress方法和split_line方法:

def progress(self, txt, *args, **kwargs):
    if self.isEnabledFor(self.PROGRESS):
        txt = txt % (args)
        for line in txt.split('\n'):
            self._log(self.PROGRESS, line, [], **kwargs)
于 2013-10-27T09:23:44.190 回答
2

正如第一个答案中所建议的,继承 Logger 类并使用 logging.setLoggerClass 应该可以解决问题。您将需要一个修改后的 findCaller 函数,该函数正在处理您的包装函数调用。

将以下内容放入模块中,因为 findCaller 类方法正在从文件中搜索第一个调用,这不是当前的源文件名。

import inspect
import logging
import os

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 WrappedLogger(logging.Logger):
    def __init__(self,name):
        logging.Logger.__init__(self, name)

    def findCaller(self):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.
        """
        # get all outer frames starting from the current frame
        outer = inspect.getouterframes(inspect.currentframe())
        # reverse the order, to search from out inward
        outer.reverse()
        rv = "(unknown file)", 0, "(unknown function)"    

        pos = 0
        # go through all frames
        for i in range(0,len(outer)):
            # stop if we find the current source filename
            if outer[i][1] == _srcfile:
                # the caller is the previous one
                pos=i-1
                break

        # get the frame (stored in first tuple entry)
        f = outer[pos][0]

        co = f.f_code
        rv = (co.co_filename, f.f_lineno, co.co_name)

        return rv
# Usage:
logging.setLoggerClass(WrappedLogger)
log = logging.getLogger("something")
于 2014-01-08T10:27:48.610 回答
2

有人给出了正确的答案。我会做一个总结。

logging.Logger.findCaller() ,它通过原始包中的logging._srcfile过滤堆栈帧。logging

所以我们做同样的事情,过滤我们自己的 logger wrapper my_log_module._srcfile。我们动态替换您的记录器实例的方法logging.Logger.findCaller()

顺便说一句,当 findCaller 时,请不要创建logging.Logger,包的子类没有面向 OOP 的设计,可怜……是吗?logging

# file: my_log_module.py, Python-2.7, define your logging wrapper here
import sys
import os
import logging
my_logger = logging.getLogger('my_log')

if hasattr(sys, '_getframe'): currentframe = lambda: sys._getframe(3)
# done filching

#
# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame.
#
_srcfile = os.path.normcase(currentframe.__code__.co_filename)

def findCallerPatch(self):
    """
    Find the stack frame of the caller so that we can note the source
    file name, line number and function name.
    """
    f = 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

# DO patch
my_logger.findCaller = findCallerPatch

好的,一切准备就绪。您现在可以在其他模块中使用您的记录器,添加您的记录消息格式:lineno, path, method name, blablabla

# file: app.py
from my_log_module import my_logger
my_logger.debug('I can check right caller now')

或者你可以使用优雅的方式,但不要使用全局logging.setLoggerClass

# file: my_log_modue.py
import logging
my_logger = logging.getLogger('my_log')

class MyLogger(logging.Logger):
    ...

my_logger.__class__ = MyLogger
于 2017-04-08T17:44:47.950 回答
0

感谢@glglgl,我可以想出广告高级 findCaller

请注意初始化_logging_srcfile_this_srcfile- 灵感来自python 日志记录源代码

当然,您可以将自己的规则放入findCaller()- 这里我只是从自定义记录器所在的文件中排除所有内容,除了test_logging函数。

重要的是,只有在将名称传递给getLogger(name)工厂时才会检索自定义记录器。如果您只是这样做logging.getLogger(),您将获得不是您的记录器的 RootLogger。

import sys
import os
import logging
# from inspect import currentframe
currentframe = lambda: sys._getframe(3)
_logging_srcfile = os.path.normcase(logging.addLevelName.__code__.co_filename)
_this_srcfile = __file__


def test_logging():
    logger = logging.getLogger('test')
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(logging.Formatter('%(funcName)s: %(message)s'))
    handler.setLevel(11)
    logger.addHandler(handler)
    logger.debug('Will not print')
    logger.your_function('Test Me')


class CustomLogger(logging.getLoggerClass()):
    def __init__(self, name, level=logging.NOTSET):
        super(CustomLogger, self).__init__(name, level)

    def your_function(self, msg, *args, **kwargs):
        # whatever you want to do here...
        self._log(12, msg, args, **kwargs)

    def findCaller(self):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.

        This function comes straight from the original python one
        """
        f = 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)
            ## original condition
            # if filename == _logging_srcfile:
            ## PUT HERE YOUR CUSTOM CONDITION, eg:
            ## skip also this file, except the test_logging method which is used for debug
            if co.co_name != 'test_logging' and filename in [_logging_srcfile, _this_srcfile]:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

logging.setLoggerClass(CustomLogger)
于 2016-07-14T13:17:13.113 回答
0

Python 3.8.6,Logging _log 函数有一个关键参数“stacklevel”,所以我解决了同样的问题:

包装类 MyLogger:

def __init__(self):
    ...
    self._logger = logging.getLogger(__name__)

def debug(self, msg, *args, **kwargs):
    self._logger.debug(msg, *args, stacklevel=2, **kwargs)

此代码在调试消息中正确显示函数名称、代码行号等

于 2021-02-25T14:41:15.243 回答