日志记录器配置以将日志记录到文件并打印到标准输出

638
我正在使用Python的logging模块将一些调试字符串记录到文件中,这个功能非常好用。现在,除此之外,我还想使用这个模块将这些字符串打印到标准输出(stdout)中。我该怎么做呢?为了将我的字符串记录到文件中,我使用以下代码:
import logging
import logging.handlers
logger = logging.getLogger("")
logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler(
    LOGFILE, maxBytes=(1048576*5), backupCount=7
)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)

然后调用一个类似的日志记录函数

logger.debug("I am written to the file")

非常感谢您的帮助!


好奇,如果您将级别更改为“DEBUG”,它是否仍会记录到stdout和文件中?我认为只有当它设置为“INFO”时才会这样。如果我错了,请纠正我。 - Charlie Parker
1
请参考以下链接:https://dev59.com/hGYq5IYBdhLWcg3w8lNo - djvg
10个回答

722

只需获取根记录器并添加 StreamHandlerStreamHandler 将写入 stderr。不确定是否真的需要 stdout 而非 stderr,但这是我在设置 Python 记录器时使用的方式,我还添加了 FileHandler。然后所有日志都会发送到两个地方(这听起来就像你想要的)。

import logging
logging.getLogger().addHandler(logging.StreamHandler())
如果您想将输出发送到stdout而不是stderr,您只需要在StreamHandler构造函数中指定即可。
import sys
# ...
logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
你也可以添加一个Formatter到它上面,这样你所有的日志行都会有一个公共的头部。
即:
import logging
logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]  %(message)s")
rootLogger = logging.getLogger()

fileHandler = logging.FileHandler("{0}/{1}.log".format(logPath, fileName))
fileHandler.setFormatter(logFormatter)
rootLogger.addHandler(fileHandler)

consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(logFormatter)
rootLogger.addHandler(consoleHandler)

打印为以下格式:

2012-12-05 16:58:26,618 [MainThread  ] [INFO ]  my message

24
你可以使用 sys.stdout 来初始化 StreamHandler,这样它就会将日志输出到标准输出而不是标准错误。 - Silas Ray
1
@sr2222 logger.addHandler(sys.stdout) 给我报错:NameError: name 'sys' is not defined - stdcerr
27
嗯,是的...你必须先import sys。然后实际上初始化处理程序,即consoleHandler = logging.StreamHandler(sys.stdout) - Silas Ray
16
因为正如我之前所说的,这不是正确的做法。使用sys.stdout创建HANDLER,然后将处理程序附加到记录器上。 - Silas Ray
16
如果您想查看信息或调试消息,请不要忘记设置rootLogger.setLevel(logging.DEBUG) - storm_m2138
显示剩余6条评论

642

logging.basicConfig() 自 Python 3.3 开始可以接受关键字参数 handlers,这大大简化了日志设置,特别是在设置具有相同格式化程序的多个处理程序时:

handlers - 如果指定了此参数,应将其设置为一个已创建的处理程序的可迭代对象以添加到根记录器中。任何没有设置格式化程序的处理程序都将被分配在此函数中创建的默认格式化程序。

因此,整个设置可以通过单个调用来完成:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("debug.log"),
        logging.StreamHandler()
    ]
)

(或者按照原问题的要求使用import sys + StreamHandler(sys.stdout) —— StreamHandler 的默认输出是 stderr。如果要自定义日志格式并添加文件名/行号、线程信息等,请查看 LogRecord 属性)。

上述设置只需要在脚本开头执行一次即可。您可以在代码库中的所有其他位置后续使用日志记录,例如:

logging.info('Useful message')
logging.error('Something bad happened')
...

注意:如果它不能正常工作,那么可能是有其他人以不同的方式初始化了日志系统。建议在调用 basicConfig() 之前执行 logging.root.handlers = []


7
别忘了设置level=logging.INFO或所需的级别。 - Andy Matteson
8
FileHandler 的定义为:logging.FileHandler(filename, mode='a', encoding=None, delay=False)。这意味着,当您只想在同一文件夹中记录日志时,可以使用 FileHandler("mylog.log")。如果您想每次覆盖日志,请将 "w" 设置为第二个参数。 - user136036
13
我尝试了这个,但输出文件是空的,即使控制台正在给出输出... 有什么建议吗? - Ramesh-X
17
@Ramesh-X,这也让我感到疯狂。在调用basicConfig之前,只需执行logging.root.handlers = [],看一下这个函数-它很烦人。 - ihadanny
3
为了让PyCharm高兴,我还需要加上noinspection PyArgumentList,原因是因为https://youtrack.jetbrains.com/issue/PY-39762。 - Weekend
显示剩余5条评论

90

如果没有传入参数,添加StreamHandler将输出到stderr而非stdout。如果其他进程依赖于stdout(例如编写NRPE插件时),请确保明确指定stdout,否则可能会遇到一些意外的麻烦。

这里是一个快速示例,重用问题中假设的值和LOGFILE:

import logging
from logging.handlers import RotatingFileHandler
from logging import handlers
import sys

log = logging.getLogger('')
log.setLevel(logging.DEBUG)
format = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

ch = logging.StreamHandler(sys.stdout)
ch.setFormatter(format)
log.addHandler(ch)

fh = handlers.RotatingFileHandler(LOGFILE, maxBytes=(1048576*5), backupCount=7)
fh.setFormatter(format)
log.addHandler(fh)

好奇,如果您将级别更改为“DEBUG”,它是否仍会记录到stdout和文件中?我认为只有当它设置为“INFO”时才会这样。如果我错了,请纠正我。 - Charlie Parker

30

这是一个完整且精心打包的解决方案,基于Waterboy的答案和其他来源。它支持向控制台和日志文件记录日志,允许设置不同的日志级别,并提供了彩色输出和易于配置的功能(也可作为Gist使用):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# -------------------------------------------------------------------------------
#                                                                               -
#  Python dual-logging setup (console and log file),                            -
#  supporting different log levels and colorized output                         -
#                                                                               -
#  Created by Fonic <https://github.com/fonic>                                  -
#  Date: 04/05/20 - 02/07/23                                                    -
#                                                                               -
#  Based on:                                                                    -
#  https://dev59.com/h2Yr5IYBdhLWcg3wXJEK#13733863                                 -
#  https://uran198.github.io/en/python/2016/07/12/colorful-python-logging.html  -
#  https://en.wikipedia.org/wiki/ANSI_escape_code#Colors                        -
#                                                                               -
# -------------------------------------------------------------------------------

# Imports
import os
import sys
import logging

# Logging formatter supporting colorized output
class LogFormatter(logging.Formatter):

    COLOR_CODES = {
        logging.CRITICAL: "\033[1;35m", # bright/bold magenta
        logging.ERROR:    "\033[1;31m", # bright/bold red
        logging.WARNING:  "\033[1;33m", # bright/bold yellow
        logging.INFO:     "\033[0;37m", # white / light gray
        logging.DEBUG:    "\033[1;30m"  # bright/bold black / dark gray
    }

    RESET_CODE = "\033[0m"

    def __init__(self, color, *args, **kwargs):
        super(LogFormatter, self).__init__(*args, **kwargs)
        self.color = color

    def format(self, record, *args, **kwargs):
        if (self.color == True and record.levelno in self.COLOR_CODES):
            record.color_on  = self.COLOR_CODES[record.levelno]
            record.color_off = self.RESET_CODE
        else:
            record.color_on  = ""
            record.color_off = ""
        return super(LogFormatter, self).format(record, *args, **kwargs)

# Set up logging
def set_up_logging(console_log_output, console_log_level, console_log_color, logfile_file, logfile_log_level, logfile_log_color, log_line_template):

    # Create logger
    # For simplicity, we use the root logger, i.e. call 'logging.getLogger()'
    # without name argument. This way we can simply use module methods for
    # for logging throughout the script. An alternative would be exporting
    # the logger, i.e. 'global logger; logger = logging.getLogger("<name>")'
    logger = logging.getLogger()

    # Set global log level to 'debug' (required for handler levels to work)
    logger.setLevel(logging.DEBUG)

    # Create console handler
    console_log_output = console_log_output.lower()
    if (console_log_output == "stdout"):
        console_log_output = sys.stdout
    elif (console_log_output == "stderr"):
        console_log_output = sys.stderr
    else:
        print("Failed to set console output: invalid output: '%s'" % console_log_output)
        return False
    console_handler = logging.StreamHandler(console_log_output)

    # Set console log level
    try:
        console_handler.setLevel(console_log_level.upper()) # only accepts uppercase level names
    except:
        print("Failed to set console log level: invalid level: '%s'" % console_log_level)
        return False

    # Create and set formatter, add console handler to logger
    console_formatter = LogFormatter(fmt=log_line_template, color=console_log_color)
    console_handler.setFormatter(console_formatter)
    logger.addHandler(console_handler)

    # Create log file handler
    try:
        logfile_handler = logging.FileHandler(logfile_file)
    except Exception as exception:
        print("Failed to set up log file: %s" % str(exception))
        return False

    # Set log file log level
    try:
        logfile_handler.setLevel(logfile_log_level.upper()) # only accepts uppercase level names
    except:
        print("Failed to set log file log level: invalid level: '%s'" % logfile_log_level)
        return False

    # Create and set formatter, add log file handler to logger
    logfile_formatter = LogFormatter(fmt=log_line_template, color=logfile_log_color)
    logfile_handler.setFormatter(logfile_formatter)
    logger.addHandler(logfile_handler)

    # Success
    return True

# Main function
def main():

    # Set up logging
    script_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
    if (not set_up_logging(console_log_output="stdout", console_log_level="warning", console_log_color=True,
                           logfile_file=script_name + ".log", logfile_log_level="debug", logfile_log_color=False,
                           log_line_template="%(color_on)s[%(created)d] [%(threadName)s] [%(levelname)-8s] %(message)s%(color_off)s")):
        print("Failed to set up logging, aborting.")
        return 1

    # Log some messages
    logging.debug("Debug message")
    logging.info("Info message")
    logging.warning("Warning message")
    logging.error("Error message")
    logging.critical("Critical message")

# Call main function
if (__name__ == "__main__"):
    sys.exit(main())

关于 Microsoft Windows:
如果想要在 Microsoft Windows 的经典 命令提示符 中显示带颜色的输出,需要添加一些额外的代码。但在新的支持带颜色的输出功能的终端应用中,则不需要这样做。

有两种选择:

1) 使用 Python 包 colorama (过滤发送到 stdout stderr 的输出并将逃逸序列转换为本地Windows API调用;适用于 Windows XP 及更高版本):

import colorama
colorama.init()

2) 使用以下函数启用ANSI终端模式(通过设置ENABLE_VIRTUAL_TERMINAL_PROCESSING标志使终端解释转义序列;有关更多信息,请参见此处此处此处此处;适用于 Windows 10 及以后版本):

# Imports
import sys
import ctypes

# Enable ANSI terminal mode for Command Prompt on Microsoft Windows
def windows_enable_ansi_terminal_mode():
    if (sys.platform != "win32"):
        return None
    try:
        kernel32 = ctypes.windll.kernel32
        result = kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
        if (result == 0): raise Exception
        return True
    except:
        return False

这个方法在Win10上对我有效:https://dev59.com/dWkw5IYBdhLWcg3wg6ug --> 在"LogFormatter(loggin.Formatter):"后面的下一行中加入"from colorama import init"和"init(convert=True)" - n8thanael
@Maxxim,您知道在多个文件中如何运作吗?我在main.py中初始化记录器,然后在其他文件中使用logging.debug等,但好像无法工作。 - Björn
@Björn,它应该像你描述的那样工作。我在我的项目中也是这样做的:在main.py中设置日志记录一次,然后在模块中导入logging + logging.debug()等。不太确定为什么在你的情况下它不起作用。 - Fonic
我真的不知道为什么它不起作用。它创建了.log文件,但在其他文件中,它只是将内容写入控制台而不是文件 :( - Björn
@Björn 你还需要在其他文件中导入 logging,是的。你不需要从 main.py 导入任何日志记录器变量。 - Fonic
显示剩余3条评论

19

在设置任何其他处理程序或记录任何消息之前,可以使用 stream=sys.stdout 作为参数运行 basicConfig,或手动添加一个将消息推送到 stdout 的 StreamHandler 到根记录器(或您想要的任何其他记录器)。


2
当我尝试同时使用streamfilename参数或处理程序时,会出现错误。 - rfii

15

记录到不同级别和格式的标准输出(stdout)轮换文件(rotating file)

import logging
import logging.handlers
import sys

if __name__ == "__main__":

    # Change root logger level from WARNING (default) to NOTSET in order for all messages to be delegated.
    logging.getLogger().setLevel(logging.NOTSET)

    # Add stdout handler, with level INFO
    console = logging.StreamHandler(sys.stdout)
    console.setLevel(logging.INFO)
    formater = logging.Formatter('%(name)-13s: %(levelname)-8s %(message)s')
    console.setFormatter(formater)
    logging.getLogger().addHandler(console)

    # Add file rotating handler, with level DEBUG
    rotatingHandler = logging.handlers.RotatingFileHandler(filename='rotating.log', maxBytes=1000, backupCount=5)
    rotatingHandler.setLevel(logging.DEBUG)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    rotatingHandler.setFormatter(formatter)
    logging.getLogger().addHandler(rotatingHandler)

    log = logging.getLogger("app." + __name__)

    log.debug('Debug message, should only appear in the file.')
    log.info('Info message, should appear in file and stdout.')
    log.warning('Warning message, should appear in file and stdout.')
    log.error('Error message, should appear in file and stdout.')

在日志消息中使用\n%s时,会出现ValueError: unsupported format character 'n' (0x6e) at index 46错误。 - JackTheKnife

7

在多个Python软件包中反复使用Waterboy的代码后,我最终将其转换为一个小巧的独立Python软件包,并且你可以在此处找到它:

https://github.com/acschaefer/duallog

该代码有很好的文档说明并且易于使用。只需下载.py文件并将其包含在你的项目中,或通过pip install duallog安装整个软件包即可。


由于某种原因,日志既没有被记录到控制台,也没有记录到文件中(文件为空)。 - JackTheKnife

1

虽然问题明确要求 日志记录器配置,但是有一种替代方法不需要对 logging 配置进行任何更改,也不需要重定向 stdout

这种方法可能有点简单,但它确实有效:

def log_and_print(message: str, level: int, logger: logging.Logger):
    logger.log(level=level, msg=message)  # log as normal
    print(message)  # prints to stdout by default

现在我们不再使用例如logger.debug('something')这样的语句,而是调用log_and_print(message='something', level=logging.DEBUG, logger=logger)

我们还可以稍微扩展一下,这样它只在必要时打印stdout

def log_print(message: str, level: int, logger: logging.Logger):
    # log the message normally
    logger.log(level=level, msg=message)
    # only print to stdout if the message is not logged to stdout
    msg_logged_to_stdout = False
    current_logger = logger
    while current_logger and not msg_logged_to_stdout:
        is_enabled = current_logger.isEnabledFor(level)
        logs_to_stdout = any(
            getattr(handler, 'stream', None) == sys.stdout
            for handler in current_logger.handlers
        )
        msg_logged_to_stdout = is_enabled and logs_to_stdout
        if not current_logger.propagate:
            current_logger = None
        else:
            current_logger = current_logger.parent            
    if not msg_logged_to_stdout:
        print(message)
    

这将检查记录器及其父级是否有任何流到stdout的处理程序,并检查记录器是否启用了指定级别。

请注意,这并没有针对性能进行优化


0

我已经通过以下模块(此处也提供了Gist链接)同时将日志和打印重定向到磁盘上的文件、标准输出和标准错误流。

import logging
import pathlib
import sys

from ml.common.const import LOG_DIR_PATH, ML_DIR


def create_log_file_path(file_path, root_dir=ML_DIR, log_dir=LOG_DIR_PATH):
    path_parts = list(pathlib.Path(file_path).parts)
    relative_path_parts = path_parts[path_parts.index(root_dir) + 1:]
    log_file_path = pathlib.Path(log_dir, *relative_path_parts)
    log_file_path = log_file_path.with_suffix('.log')
    # Create the directories and the file itself
    log_file_path.parent.mkdir(parents=True, exist_ok=True)
    log_file_path.touch(exist_ok=True)
    return log_file_path


def set_up_logs(file_path, mode='a', level=logging.INFO):
    log_file_path = create_log_file_path(file_path)
    logging_handlers = [logging.FileHandler(log_file_path, mode=mode),
                        logging.StreamHandler(sys.stdout)]
    logging.basicConfig(
        handlers=logging_handlers,
        format='%(asctime)s %(name)s %(levelname)s %(message)s',
        level=level
    )


class OpenedFileHandler(logging.FileHandler):

    def __init__(self, file_handle, filename, mode):
        self.file_handle = file_handle
        super(OpenedFileHandler, self).__init__(filename, mode)

    def _open(self):
        return self.file_handle


class StandardError:
    def __init__(self, buffer_stderr, buffer_file):
        self.buffer_stderr = buffer_stderr
        self.buffer_file = buffer_file

    def write(self, message):
        self.buffer_stderr.write(message)
        self.buffer_file.write(message)


class StandardOutput:
    def __init__(self, buffer_stdout, buffer_file):
        self.buffer_stdout = buffer_stdout
        self.buffer_file = buffer_file

    def write(self, message):
        self.buffer_stdout.write(message)
        self.buffer_file.write(message)


class Logger:
    def __init__(self, file_path, mode='a', level=logging.INFO):
        self.stdout_ = sys.stdout
        self.stderr_ = sys.stderr

        log_file_path = create_log_file_path(file_path)
        self.file_ = open(log_file_path, mode=mode)

        logging_handlers = [OpenedFileHandler(self.file_, log_file_path,
                                              mode=mode),
                            logging.StreamHandler(sys.stdout)]
        logging.basicConfig(
            handlers=logging_handlers,
            format='%(asctime)s %(name)s %(levelname)s %(message)s',
            level=level
        )

    # Overrides write() method of stdout and stderr buffers
    def write(self, message):
        self.stdout_.write(message)
        self.stderr_.write(message)
        self.file_.write(message)

    def flush(self):
        pass

    def __enter__(self):
        sys.stdout = StandardOutput(self.stdout_, self.file_)
        sys.stderr = StandardError(self.stderr_, self.file_)

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self.stdout_
        sys.stderr = self.stderr_
        self.file_.close()

我们可以将其编写成上下文管理器,在你的Python脚本中只需添加一行代码,就可以轻松地加入该功能:

from logger import Logger

...

if __name__ == '__main__':
    with Logger(__file__):
        main()


-5

对于2.7版本,请尝试以下操作:

fh = logging.handlers.RotatingFileHandler(LOGFILE, maxBytes=(1048576*5), backupCount=7)

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