将sys.stdout重定向到Python日志

11 投票
2 回答
13806 浏览
提问于 2025-04-15 12:09

现在我们有很多Python脚本,想把它们合并起来,并修复一些重复的部分。我们正在做的一件事是确保所有的sys.stdout和sys.stderr的输出都能进入Python的日志模块。

我们主要想打印出以下内容:

[<ERROR LEVEL>] | <TIME> | <WHERE> | <MSG>

现在,几乎所有的Python错误信息在sys.stdout和sys.stderr中的格式都是[LEVEL] - MSG,这些信息都是通过sys.stdout和sys.stderr输出的。我可以在我的sys.stdout和sys.stderr的包装器中解析这些信息,然后根据解析的内容调用相应的日志级别。

简单来说,我们有一个叫做foo的包,还有一个叫做log的子包。在__init__.py文件中,我们定义了以下内容:

def initLogging(default_level = logging.INFO, stdout_wrapper = None, \
                stderr_wrapper = None):
    """
        Initialize the default logging sub system
    """
    root_logger = logging.getLogger('')
    strm_out = logging.StreamHandler(sys.__stdout__)
    strm_out.setFormatter(logging.Formatter(DEFAULT_LOG_TIME_FORMAT, \
                                            DEFAULT_LOG_TIME_FORMAT))
    root_logger.setLevel(default_level)
    root_logger.addHandler(strm_out)

    console_logger = logging.getLogger(LOGGER_CONSOLE)
    strm_out = logging.StreamHandler(sys.__stdout__)
    #strm_out.setFormatter(logging.Formatter(DEFAULT_LOG_MSG_FORMAT, \
    #                                        DEFAULT_LOG_TIME_FORMAT))
    console_logger.setLevel(logging.INFO)
    console_logger.addHandler(strm_out)

    if stdout_wrapper:
        sys.stdout = stdout_wrapper
    if stderr_wrapper:
        sys.stderr = stderr_wrapper


def cleanMsg(msg, is_stderr = False):
    logy = logging.getLogger('MSG')
    msg = msg.rstrip('\n').lstrip('\n')
    p_level = r'^(\s+)?\[(?P<LEVEL>\w+)\](\s+)?(?P<MSG>.*)$'
    m = re.match(p_level, msg)
    if m:
        msg = m.group('MSG')
        if m.group('LEVEL') in ('WARNING'):
            logy.warning(msg)
            return
        elif m.group('LEVEL') in ('ERROR'):
            logy.error(msg)
            return
    if is_stderr:
        logy.error(msg)
    else:
        logy.info(msg)

class StdOutWrapper:
    """
        Call wrapper for stdout
    """
    def write(self, s):
        cleanMsg(s, False)

class StdErrWrapper:
    """
        Call wrapper for stderr
    """
    def write(self, s):
        cleanMsg(s, True)

然后我们会在我们的某个脚本中调用这个,比如:

import foo.log
foo.log.initLogging(20, foo.log.StdOutWrapper(), foo.log.StdErrWrapper())
sys.stdout.write('[ERROR] Foobar blew')

这会被转换成一个错误日志信息,像这样:

[ERROR] | 20090610 083215 | __init__.py | Foobar Blew

现在问题是,当我们这样做时,错误信息被记录的模块变成了__init__(对应于foo.log.__init__.py文件),这就违背了我们的初衷。

我尝试对stderr/stdout对象进行深拷贝和浅拷贝,但这没有任何效果,信息仍然显示发生错误的模块是__init__.py。我该怎么做才能避免这种情况呢?

2 个回答

2

我觉得问题在于,你的实际日志信息现在是通过 logy.errorlogy.infocleanMsg 方法中生成的,所以这个方法就是日志信息的来源,而你看到的内容来自于 __init__.py

如果你查看 Python 的 lib/logging/__init__.py 源码,你会发现里面有一个叫 findCaller 的方法,这个方法是日志模块用来找出哪个地方发起了日志请求的。
也许你可以在你的日志对象上重写这个方法,以自定义它的行为?

6

这个问题是,日志模块只向上查找调用栈的一层来找出是谁调用了它,但现在你的函数在这个时候成了一个中间层(虽然我本来以为它会报告cleanMsg,而不是__init__,因为你是在这里调用log()的)。所以,你需要让它向上查找两层,或者把调用者的信息传递到日志消息中。你可以通过自己查看调用栈来获取调用函数,并把它插入到消息中。

要找到你的调用帧,你可以使用inspect模块:

import inspect
f = inspect.currentframe(N)

这个方法会向上查找N层帧,并返回帧指针。也就是说,你的直接调用者是currentframe(1),但如果这是stdout.write方法,你可能需要再向上查找一层。

一旦你有了调用帧,你可以获取正在执行的代码对象,并查看与之相关的文件和函数名。例如:

code = f.f_code
caller = '%s:%s' % (code.co_filename, code.co_name)

你可能还需要添加一些代码来处理非Python代码对你的调用(比如C函数或内置函数),因为这些可能没有f_code对象。

另外,参考mikej的回答,你可以在一个自定义的Logger类中使用相同的方法,这个类继承自logging.Logger,并重写findCaller方法,以便向上查找多个帧,而不是只查找一层。

撰写回答