将sys.stdout重定向到Python日志
现在我们有很多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 个回答
我觉得问题在于,你的实际日志信息现在是通过 logy.error
和 logy.info
在 cleanMsg
方法中生成的,所以这个方法就是日志信息的来源,而你看到的内容来自于 __init__.py
。
如果你查看 Python 的 lib/logging/__init__.py
源码,你会发现里面有一个叫 findCaller
的方法,这个方法是日志模块用来找出哪个地方发起了日志请求的。
也许你可以在你的日志对象上重写这个方法,以自定义它的行为?
这个问题是,日志模块只向上查找调用栈的一层来找出是谁调用了它,但现在你的函数在这个时候成了一个中间层(虽然我本来以为它会报告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方法,以便向上查找多个帧,而不是只查找一层。