我使用Python的日志模块将一些调试字符串记录到一个工作得很好的文件中。另外,我还想使用这个模块将字符串打印到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")

谢谢你的帮助!


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


只需获得根记录器的句柄并添加StreamHandler。StreamHandler写入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,以便所有日志行都有一个公共标题。

ie:

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

添加不带参数的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)

对于2.7,尝试以下操作:

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

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

handlers——如果指定,这应该是一个已创建的要添加到根日志记录器的处理程序的可迭代对象。任何还没有格式化器集的处理程序将被分配在此函数中创建的默认格式化器。

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

import logging

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

(或根据原始问题的要求导入sys + StreamHandler(sys.stdout) -默认的StreamHandler是写入stderr。如果你想自定义日志格式并添加文件名/行、线程信息等,请查看LogRecord属性。)

上面的设置只需要在脚本的开头执行一次。你可以在代码库的其他地方使用日志,如下所示:

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

注意:如果它不起作用,那么其他人可能已经对日志系统进行了不同的初始化。注释建议在调用basicConfig()之前执行logging.root.handlers =[]。


在多个Python包中反复使用Waterboy的代码后,我最终将其转换为一个独立的小Python包,你可以在这里找到:

https://github.com/acschaefer/duallog

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


记录到标准输出和旋转文件的不同级别和格式:

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.')

下面是一个完整的、包装精美的解决方案,基于水男孩的答案和各种其他来源。它支持日志记录到控制台和日志文件,允许不同的日志级别设置,提供彩色输出,并且易于配置(也可作为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://stackoverflow.com/a/13733863/1976617                                 -
#  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: 为了让彩色输出在微软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

我已经处理了将日志和打印同时重定向到磁盘、stdout和stderr上的文件,通过以下模块(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()


尽管问题特别要求记录器配置,但有一种替代方法不需要对日志记录配置进行任何更改,也不需要重定向标准输出。

也许有点简单,但它确实有效:

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

我们现在调用log_and_print(message='something', level=logging. debug('something')而不是例如logger.debug('something')。调试日志记录器=记录器)。

我们还可以稍微扩展一下,这样它只在必要时才打印到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)
    

这将检查日志记录器及其父日志记录器是否有任何流到标准输出的处理程序,并检查日志记录器是否为指定级别启用。

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