如何在Python中将stdout和stderr重定向到日志记录器

87

我有一个带有RotatingFileHandler的记录器。 我想将所有的StdoutStderr重定向到记录器。 如何实现?


你是否有直接写入文件描述符1和2的外部模块/库? - Ignacio Vazquez-Abrams
@IgnacioVazquez-Abrams 我不是很理解您的意思,但我会尝试解释一下。我正在使用几个Python进程,并且我希望从所有进程中重定向所有 stdoutstderr 消息到我的记录器。 - orenma
11个回答

57

我没有足够的声望来发表评论,但我希望在这里分享我的解决方法,以便其他人也可以从中受益。

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)

2
由于flush方法,我得到了一个奇怪的输出:warning archan_pylint:18: <archan_pylint.LoggerWriter object at 0x7fde3cfa2208>。看起来是stderr对象被打印出来而不是换行符或其他东西,所以我只是删除了flush方法,现在似乎可以工作了。 - pawamoy
1
@Cameron,请看一下我下面的答案,以提高输出可读性。 - Toni Homedes i Saun
1
它可以在Python2和3中使用,如果您记录到文件中(例如logging.basicConfig(filename ='example.log',level = logging.DEBUG))。但是,如果您想要例如logging.basicConfig(stream = sys.stdout,level = logging.DEBUG),那么它不起作用(在python3上也会导致堆栈溢出)。 (我猜因为它捕获了std out),因此对于从Kubernetes pod记录到std out等不太有用。 请注意,由shellcat_zero发现的代码也可以使用stream = sys.stdout。 - Attila123
def flush(self): pass 避免将 <archan_pylint.LoggerWriter object at 0x7fde3cfa2208> 打印到日志中。 - Alon Gouldman

42

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/,在这里他演示了如何将标准输出和标准错误同时发送到记录器(Logger):

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 = '' 是处理清空缓冲区的地方,而不是实现一个flush函数。


6
这段代码的许可证为GPL。我不确定它是否能够发布到要求与CC by-sa兼容的 SO 网站上。 - asmeurer
7
我为什么会收到这个错误信息呢?“Exception ignored in: <__main__.StreamToLogger object at 0x7f72a6fbe940> AttributeError ‘StreamToLogger’对象没有‘flush’属性。” - soungalo
删除代码片段的最后两行,错误消息就会消失... - Preetkaran Singh
1
更“安全”的方法是扩展TextIOBase。我的库中的某个地方调用了sys.stdout.isatty(),而StreamToLogger由于没有属性“isatty”而失败。在我定义了类StreamToLogger(TextIOBase)之后,它就可以工作了。 - James H
1
这并不适用于我的情况。在我的代码中,我正在执行:check_call(command, shell=True, stdout=sys.stdout, stderr=sys.stderr )。这会导致错误,因为Python在代码深处执行了以下操作:c2pwrite = stdout.fileno()。 - Pablo
如果您想恢复默认的stdout设置,请参见例如这里 - Aelius

18

如果是一个全Python系统(即没有C库直接写入fds,就像Ignacio Vazquez-Abrams所问的那样),那么您可以尝试使用这里建议的方法:

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.stdoutsys.stderr设置为LoggerWriter实例。


谢谢,它完成了任务,但由于某种原因stderr将其消息逐个单词发送,您知道为什么吗? - orenma
@orenma 可能是因为 write 是逐字逐句地调用的。您可以根据自己的需要修改我的示例代码。 - Vinay Sajip
如果在重定向stderr之后调用sys.stderr.flush()会发生什么? - Moberg
@Moberg 那么你也必须定义一个 flush() 方法。 - Vinay Sajip
1
我无法使库代码不使用sys.stderr.flush()等。最好的处理所有属性的方法是什么? - Moberg
2
如果涉及到C库呢?那该怎么办?如何让C库输出到同一个LoggerWriter? - azmath

17

您可以使用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')

6
这是Python官方文档中contextlib模块的一部分内容:「上下文管理器,用于将sys.stdout临时重定向到另一个文件或类文件对象。」请注意,对sys.stdout的全局副作用意味着该上下文管理器不适合在库代码和大多数线程应用程序中使用。它也对子进程的输出没有影响。然而,对于许多实用脚本来说,这仍然是一种有用的方法。因此,在覆盖整个应用程序(例如,我有一个运行grpc服务器的微服务,在提供请求时启动线程)时,可能会感到非常不便(如果可能的话)。 - Attila123
这里的其他解决方案是否也会对 sys.stdout/sys.stderr 产生相同的全局副作用呢?@Attila123 - xjcl

15

正确地进行输出重定向!

问题

logger.log 和其他函数(如.info/.error/等等)会将每次调用作为单独的一行输出,也就是隐式地添加(格式化和)换行符

另一方面,sys.stderr.write 只是将文字输入直接写入流中,包括不完整的行。例如:输出 "ZeroDivisionError: division by zero" 实际上是对 sys.stderr.write 进行了 4 次(!) 单独的调用:

sys.stderr.write('ZeroDivisionError')
sys.stderr.write(': ')
sys.stderr.write('division by zero')
sys.stderr.write('\n')

排名前四的最受赞同的方法(1234)因此会产生额外的换行符--只需在程序中输入“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(夏列米尔画家算法)。简而言之:这样可以将时间复杂度从潜在的O(n^2)降低到O(n)

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以下的版本中,你可以用msg.rstrip('\n')或者msg[:-1]代替msg.removesuffix('\n')


不错,但您假设如果msg中有'\n',它将始终在msg的末尾,并且单个msg永远不会有多个'\n'。这在大多数当前的Python实现中可能是正确的,但我不确定它是否被定义为语言标准,因此我更喜欢“每次检查”的方法。这并不像看起来那么糟糕,因为每次Shlemiel获得新的油漆桶(一个'\n')时,他都会将其带到当前的绘画点,从零开始重新开始。 - Toni Homedes i Saun
2
@ToniHomedesiSaun:带有 '\n' 的消息是可以的,它会作为多行日志打印出来,但正如你所说,大多数内部错误消息都是对 sys.stderr 的分块调用,会显示为单独的日志。但我想如果您不想要多行日志,也可以使用msg.split('\n') - xjcl
我尝试了您的Python 3.9.16解决方案,但我的文件处理程序中没有任何日志记录。您能否请看一下?谢谢!https://pastebin.com/fKweeqYQ - Jim B
1
@JimB 你传递了错误的对象。正如名称“logfct”所示,你必须传递一个logger = logging.getLogger()对象的.info函数。相反,你正在传递logging.INFO,它只是代表日志级别的常量,即数字20 - xjcl

11

针对Cameron Gagnon的回答,我进一步改进了LoggerWriter类,以:

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: ''

1
你说得对,顶部的答案在例如异常情况下会产生错误的换行符。我的答案遵循非常相似的方法。 - xjcl

6

一个快速但易碎的单行命令

sys.stdout.write = logger.info

sys.stderr.write = logger.error

这个功能的作用就是将日志记录器函数分配给stdout/stderr的.write调用,这意味着任何写入调用都会代替调用日志记录器函数。
这种方法的缺点是写入调用和日志记录器函数通常都添加换行符,因此在日志文件中会出现多余的行,这可能是一个问题,也可能不是,具体取决于您的使用情况。
另一个陷阱是,如果您的日志记录器本身写入stderr,则会产生无限递归(堆栈溢出错误)。因此,只输出到文件。

5

Vinay Sajip的回答中加入flush后:

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

2
请注意,像这里所做的那样使用空的flush()方法是可以的,因为日志处理程序在内部处理刷新:https://dev59.com/22Qn5IYBdhLWcg3wto5W#16634444 - OmerB

2

解决StreamHandler导致无限递归的问题

我的日志记录器引起了无限递归,因为Streamhandler试图写入stdout,而stdout本身是一个日志记录器,从而导致无限递归。

解决方案

仅为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()

我曾经遇到过同样的问题。sys.stdout 真正解决了这个问题。现在,我们可以将数据同时输出到控制台和文件中。 - kkk

0
如果您想将信息和错误消息记录到不同的流中(信息记录到stdout,错误记录到stderr),可以使用以下技巧:
class ErrorStreamHandler(log.StreamHandler):
"""Print input log-message into stderr, print only error/warning messages"""
def __init__(self, stream=sys.stderr):
    log.Handler.__init__(self, log.WARNING)
    self.stream = stream

def emit(self, record):
    try:
        if record.levelno in (log.INFO, log.DEBUG, log.NOTSET):
            return
        msg = self.format(record)
        stream = self.stream
        # issue 35046: merged two stream.writes into one.
        stream.write(msg + self.terminator)
        self.flush()
    except RecursionError:  # See issue 36272
        raise
    except Exception:
        self.handleError(record)


class OutStreamHandler(log.StreamHandler):
"""Print input log-message into stdout, print only info/debug messages"""
def __init__(self, loglevel, stream=sys.stdout):
    log.Handler.__init__(self, loglevel)
    self.stream = stream

def emit(self, record):
    try:
        if record.levelno not in (log.INFO, log.DEBUG, log.NOTSET):
            return
        msg = self.format(record)
        stream = self.stream
        # issue 35046: merged two stream.writes into one.
        stream.write(msg + self.terminator)
        self.flush()
    except RecursionError:  # See issue 36272
        raise
    except Exception:
        self.handleError(record)

使用方法:

log.basicConfig(level=settings.get_loglevel(),
                    format="[%(asctime)s] %(levelname)s: %(message)s",
                    datefmt='%Y/%m/%d %H:%M:%S', handlers=[ErrorStreamHandler(), OutStreamHandler(settings.get_loglevel())])

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