如何将标准输出和标准错误重定向到 Python 中的记录器
Posted
技术标签:
【中文标题】如何将标准输出和标准错误重定向到 Python 中的记录器【英文标题】:How to redirect stdout and stderr to logger in Python 【发布时间】:2021-12-16 12:47:11 【问题描述】:我有一个有RotatingFileHandler
的记录器。
我想将所有Stdout
和Stderr
重定向到记录器。
该怎么做?
【问题讨论】:
您有直接写入 FD 1 和 2 的外部模块/库吗? @IgnacioVazquez-Abrams 我不太明白你的意思,但我会尽力解释。我正在使用几个 python 进程,我想从所有这些进程中将所有stdout
和 stderr
消息重定向到我的记录器。
How do I duplicate sys.stdout to a log file in python?的可能重复
【参考方案1】:
解决StreamHandler导致无限递归的问题
我的记录器导致了无限递归,因为 Streamhandler 试图写入标准输出,它本身就是一个记录器 -> 导致无限递归。
解决方案
仅为 StreamHandler 恢复原来的sys.__stdout__
,以便您仍然可以看到终端中显示的日志。
class DefaultStreamHandler(logging.StreamHandler):
def __init__(self, stream=sys.__stdout__):
# Use the original sys.__stdout__ to write to stdout
# for this handler, as sys.stdout will write out to logger.
super().__init__(stream)
class LoggerWriter(io.IOBase):
"""Class to replace the stderr/stdout calls to a logger"""
def __init__(self, logger_name: str, log_level: int):
""":param logger_name: Name to give the logger (e.g. 'stderr')
:param log_level: The log level, e.g. logging.DEBUG / logging.INFO that
the MESSAGES should be logged at.
"""
self.std_logger = logging.getLogger(logger_name)
# Get the "root" logger from by its name (i.e. from a config dict or at the bottom of this file)
# We will use this to create a copy of all its settings, except the name
app_logger = logging.getLogger("myAppsLogger")
[self.std_logger.addHandler(handler) for handler in app_logger.handlers]
self.std_logger.setLevel(app_logger.level) # the minimum lvl msgs will show at
self.level = log_level # the level msgs will be logged at
self.buffer = []
def write(self, msg: str):
"""Stdout/stderr logs one line at a time, rather than 1 message at a time.
Use this function to aggregate multi-line messages into 1 log call."""
msg = msg.decode() if issubclass(type(msg), bytes) else msg
if not msg.endswith("\n"):
return self.buffer.append(msg)
self.buffer.append(msg.rstrip("\n"))
message = "".join(self.buffer)
self.std_logger.log(self.level, message)
self.buffer = []
def replace_stderr_and_stdout_with_logger():
"""Replaces calls to sys.stderr -> logger.info & sys.stdout -> logger.error"""
# To access the original stdout/stderr, use sys.__stdout__/sys.__stderr__
sys.stdout = LoggerWriter("stdout", logging.INFO)
sys.stderr = LoggerWriter("stderr", logging.ERROR)
if __name__ == __main__():
# Load the logger & handlers
logger = logging.getLogger("myAppsLogger")
logger.setLevel(logging.DEBUG)
# HANDLER = logging.StreamHandler()
HANDLER = DefaultStreamHandler() # <--- replace the normal streamhandler with this
logger.addHandler(HANDLER)
logFormatter = logging.Formatter("[%(asctime)s] - %(name)s - %(levelname)s - %(message)s")
HANDLER.setFormatter(logFormatter)
# Run this AFTER you load the logger
replace_stderr_and_stdout_with_logger()
在你初始化你的记录器(代码的最后一点)之后,最后调用replace_stderr_and_stdout_with_logger()
【讨论】:
【参考方案2】:输出重定向正确!
问题
logger.log
和其他函数 (.info
/.error
/etc.) 将每个调用输出为单独的行,即隐式添加(格式化和)换行符。
另一方面,
sys.stderr.write
只是将其 文字输入 写入流,包括部分行。例如:输出“ZeroDivisionError: 除以零”实际上是对sys.stderr.write
的 4(!) 次单独调用:
sys.stderr.write('ZeroDivisionError')
sys.stderr.write(': ')
sys.stderr.write('division by zero')
sys.stderr.write('\n')
最受好评的 4 种方法(1、2、3、4)因此会产生额外的换行符——只需将“1/0”放入您的程序中,您将获得以下信息:
2021-02-17 13:10:40,814 - ERROR - ZeroDivisionError
2021-02-17 13:10:40,814 - ERROR - :
2021-02-17 13:10:40,814 - ERROR - division by zero
解决方案
将中间写入存储在缓冲区中。我使用列表作为缓冲区而不是字符串的原因是为了避免Shlemiel the painter’s algorithm。 TLDR:它是 O(n) 而不是可能的 O(n^2)
class LoggerWriter:
def __init__(self, logfct):
self.logfct = logfct
self.buf = []
def write(self, msg):
if msg.endswith('\n'):
self.buf.append(msg.removesuffix('\n'))
self.logfct(''.join(self.buf))
self.buf = []
else:
self.buf.append(msg)
def flush(self):
pass
# To access the original stdout/stderr, use sys.__stdout__/sys.__stderr__
sys.stdout = LoggerWriter(logger.info)
sys.stderr = LoggerWriter(logger.error)
2021-02-17 13:15:22,956 - ERROR - ZeroDivisionError: division by zero
对于 Python 3.9 以下的版本,您可以将替换 .removesuffix('\n')
替换为不太准确的 .rstrip('\n')
。
【讨论】:
很好,但你假设如果味精中有一个'\n',它总是在味精的末尾,并且单个味精永远不会超过一个'\n'。这在大多数当前的 Python 实现中可能是正确的,但我不确定它是否被定义为语言标准,所以我更喜欢“每次检查”的方法。这并不像看起来那么糟糕,因为每次 Shlemiel 得到一个新的油漆桶(一个“\n”),他都会把它带到当前的油漆点,从零开始。 @ToniHomedesiSaun 带有 '\n's 的消息没问题,只会打印为多行日志,但正如您所说,大多数内部错误消息都是对sys.stderr
的分块调用,并将显示为单独的日志。但我想你也可以msg.split('\n')
如果你对多行日志不满意。【参考方案3】:
快速但易碎的单线器
sys.stdout.write = logger.info
sys.stderr.write = logger.error
这样做只是将记录器函数分配给 stdout/stderr .write
调用,这意味着任何写入调用都将调用记录器函数。
这种方法的缺点是对.write
的调用和记录器函数通常都会添加一个换行符,因此您最终会在日志文件中添加额外的行,这可能会或可能不会成为问题,具体取决于您的用例.
另一个陷阱是,如果您的记录器写入 stderr 本身,我们会得到无限递归(堆栈溢出错误)。所以只输出到一个文件。
【讨论】:
【参考方案4】:Python 3 更新:
包含一个虚拟刷新函数,可防止预期函数出现错误(Python 2 只需linebuf=''
即可)。
请注意,如果从解释器会话记录与从文件运行,您的输出(和日志级别)看起来会有所不同。从文件运行会产生预期的行为(以及下面的输出)。
我们仍然消除了其他解决方案没有的额外换行符。
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, level):
self.logger = logger
self.level = level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.level, line.rstrip())
def flush(self):
pass
然后用类似的东西进行测试:
import StreamToLogger
import sys
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
filename='out.log',
filemode='a'
)
log = logging.getLogger('foobar')
sys.stdout = StreamToLogger(log,logging.INFO)
sys.stderr = StreamToLogger(log,logging.ERROR)
print('Test to standard out')
raise Exception('Test to standard error')
请参阅下面的旧 Python 2.x 答案和示例输出:
所有先前的答案似乎都存在在不需要的地方添加额外的换行符的问题。最适合我的解决方案来自 http://www.electricmonk.nl/log/2011/08/14/redirect-stdout-and-stderr-to-a-logger-in-python/,他在其中演示了如何将 stdout 和 stderr 发送到记录器:
import logging
import sys
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
filename="out.log",
filemode='a'
)
stdout_logger = logging.getLogger('STDOUT')
sl = StreamToLogger(stdout_logger, logging.INFO)
sys.stdout = sl
stderr_logger = logging.getLogger('STDERR')
sl = StreamToLogger(stderr_logger, logging.ERROR)
sys.stderr = sl
print "Test to standard out"
raise Exception('Test to standard error')
输出如下:
2011-08-14 14:46:20,573:INFO:STDOUT:Test to standard out
2011-08-14 14:46:20,573:ERROR:STDERR:Traceback (most recent call last):
2011-08-14 14:46:20,574:ERROR:STDERR: File "redirect.py", line 33, in
2011-08-14 14:46:20,574:ERROR:STDERR:raise Exception('Test to standard error')
2011-08-14 14:46:20,574:ERROR:STDERR:Exception
2011-08-14 14:46:20,574:ERROR:STDERR::
2011-08-14 14:46:20,574:ERROR:STDERR:Test to standard error
请注意,self.linebuf = ''
是处理刷新的地方,而不是实现刷新功能。
【讨论】:
此代码已获得GPL 的许可。我不确定它是否甚至可以发布在 SO 上,这需要与 CC by-sa 兼容。 知道为什么我会收到此错误消息吗? “异常被忽略:<__main__.streamtologger object at> AttributeError 'StreamToLogger' object has no attribute 'flush'” 去掉最后两行代码sn -p,错误信息消失.... 扩展 TextIOBase 更“安全”。我图书馆的某个地方正在调用 sys.stdout.isatty() 并且 StreamToLogger 由于没有属性“isatty”而失败。它在我定义类 StreamToLogger(TextIOBase) 后工作。【参考方案5】:作为对 Cameron Gagnon 响应的改进,我将 LoggerWriter
class 改进为:
class LoggerWriter(object):
def __init__(self, writer):
self._writer = writer
self._msg = ''
def write(self, message):
self._msg = self._msg + message
while '\n' in self._msg:
pos = self._msg.find('\n')
self._writer(self._msg[:pos])
self._msg = self._msg[pos+1:]
def flush(self):
if self._msg != '':
self._writer(self._msg)
self._msg = ''
现在不受控制的异常看起来更好:
2018-07-31 13:20:37,482 - ERROR - Traceback (most recent call last):
2018-07-31 13:20:37,483 - ERROR - File "mf32.py", line 317, in <module>
2018-07-31 13:20:37,485 - ERROR - main()
2018-07-31 13:20:37,486 - ERROR - File "mf32.py", line 289, in main
2018-07-31 13:20:37,488 - ERROR - int('')
2018-07-31 13:20:37,489 - ERROR - ValueError: invalid literal for int() with base 10: ''
【讨论】:
你是对的,最重要的答案会产生虚假的换行符,例如例外。我的回答遵循非常相似的方法。【参考方案6】:在 Vinay Sajip 的回答中添加了同花顺:
class LoggerWriter:
def __init__(self, logger, level):
self.logger = logger
self.level = level
def write(self, message):
if message != '\n':
self.logger.log(self.level, message)
def flush(self):
pass
【讨论】:
请注意,在这里使用空的flush()
方法是可以的,因为日志处理程序在内部处理刷新:***.com/a/16634444/8425408【参考方案7】:
您可以使用 redirect_stdout 上下文管理器:
import logging
from contextlib import redirect_stdout
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logging.write = lambda msg: logging.info(msg) if msg != '\n' else None
with redirect_stdout(logging):
print('Test')
或者像这样
import logging
from contextlib import redirect_stdout
logger = logging.getLogger('Meow')
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
fmt='[name] asctime levelname: message',
datefmt='%m/%d/%Y %H:%M:%S',
style=''
)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)
logger.write = lambda msg: logger.info(msg) if msg != '\n' else None
with redirect_stdout(logger):
print('Test')
【讨论】:
docs.python.org/3/library/contextlib.html :“用于临时将 sys.stdout 重定向到另一个文件或类似文件的对象的上下文管理器。” “请注意,对 sys.stdout 的全局副作用意味着此上下文管理器不适合在库代码和大多数线程应用程序中使用。它对子进程的输出也没有影响。但是,对于许多人来说,它仍然是一种有用的方法实用程序脚本。”所以覆盖整个应用程序似乎很不方便(如果可能的话)(例如,我有一个运行 grpc 服务器的微服务,它在服务请求时启动线程)。 这里的其他解决方案不也对sys.stdout
/sys.stderr
有相同的全局副作用吗? @Attila123【参考方案8】:
没有足够的代表发表评论,但我想添加对我有用的版本,以防其他人处于类似情况。
class LoggerWriter:
def __init__(self, level):
# self.level is really like using log.debug(message)
# at least in my case
self.level = level
def write(self, message):
# if statement reduces the amount of newlines that are
# printed to the logger
if message != '\n':
self.level(message)
def flush(self):
# create a flush method so things can be flushed when
# the system wants to. Not sure if simply 'printing'
# sys.stderr is the correct way to do it, but it seemed
# to work properly for me.
self.level(sys.stderr)
这看起来像:
log = logging.getLogger('foobar')
sys.stdout = LoggerWriter(log.debug)
sys.stderr = LoggerWriter(log.warning)
【讨论】:
由于 flush 方法,我得到了一个奇怪的输出:warning archan_pylint:18: <archan_pylint.LoggerWriter object at 0x7fde3cfa2208>
。似乎打印了 stderr 对象而不是换行符,所以我刚刚删除了 flush 方法,它现在似乎可以工作了。
@Cameron 请在下面查看我的答案,以提高输出可读性。
它适用于 python2 和 3,以防你登录到一个文件(例如 logging.basicConfig(filename='example.log', level=logging.DEBUG)。但是如果你想要例如日志记录.basicConfig(stream=sys.stdout, level=logging.DEBUG) 然后它不起作用(在 python3 上它也会导致堆栈溢出)。(我猜是因为它捕获标准输出),所以对于从 Kubernetes 进行日志记录不是那么有用pod 到标准输出。请注意,shellcat_zero 找到的代码也适用于 stream=sys.stdout。
def flush(self): pass
避免将<archan_pylint.LoggerWriter object at 0x7fde3cfa2208>
打印到日志中【参考方案9】:
如果它是一个全 Python 系统(即没有 C 库直接写入 fds,正如 Ignacio Vazquez-Abrams 所问的那样),那么您也许可以使用建议的方法 here:
class LoggerWriter:
def __init__(self, logger, level):
self.logger = logger
self.level = level
def write(self, message):
if message != '\n':
self.logger.log(self.level, message)
然后将sys.stdout
和sys.stderr
设置为LoggerWriter
实例。
【讨论】:
谢谢你,完成了这项工作,但出于某种原因stderr
将每个单词分别发送消息,你知道为什么吗?
@orenma 大概是因为 write 是逐字调用的。您可以调整我的示例代码以更贴近您的需求。
重定向stderr后调用sys.stderr.flush()会怎样?
我不能让库代码不使用 sys.stderr .flush() 等。处理其所有属性的最佳方法是什么?
如果涉及到 C 库怎么办?然后呢?如何让 C 库输出到同一个 LoggerWriter?以上是关于如何将标准输出和标准错误重定向到 Python 中的记录器的主要内容,如果未能解决你的问题,请参考以下文章