将sys.stdout重定向到Python日志记录

11

目前我们有许多Python脚本,我们正试图将它们整合并修复冗余。我们正在尝试的其中一件事情是,确保所有的sys.stdout/sys.stderr都进入Python日志记录模块。

现在主要的问题是,我们希望以下内容被打印出来:

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

现在所有的Python错误消息中,几乎所有的sys.stdout/sys.stderr消息都是以[LEVEL] - MSG的格式编写的。我可以在我的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个回答

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的答案,使用继承自logging.Logger的自定义Logger类,并重写findCaller方法向上导航多个帧,而不是一个。

2
我认为问题在于您实际的日志消息是由“cleanMsg”中的“logy.error”和“logy.info”调用创建的,因此该方法是日志消息的来源,您在“__init__.py”中看到了这一点。
如果您查看Python的“lib/logging/__init__.py”的源代码,您将看到一个名为“findCaller”的方法,这是日志模块用于派生日志请求的调用者的方法。或许您可以在日志对象上覆盖它以自定义行为?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接