前段时间,我看到一个Mono应用程序的输出是彩色的,可能是因为它的日志系统(因为所有的消息都是标准化的)。

现在,Python有了日志记录模块,它允许您指定许多选项来定制输出。所以,我想象类似的事情可能与Python,但我不知道如何在任何地方做到这一点。

是否有办法使Python日志模块输出为彩色?

我想要的(例如)错误显示为红色,调试消息显示为蓝色或黄色,等等。

当然,这可能需要一个兼容的终端(大多数现代终端都是);但如果不支持颜色,我可以退回到原始的日志输出。

有什么想法,我可以得到彩色输出与日志模块?


当前回答

一个简单但非常灵活的工具为任何终端文本着色是'colout'。

pip install colout
myprocess | colout REGEX_WITH_GROUPS color1,color2...

其中'myprocess'输出中的任何匹配正则表达式第1组的文本都将用color1着色,第2组用color2着色,等等。

例如:

tail -f /var/log/mylogfile | colout '^(\w+ \d+ [\d:]+)|(\w+\.py:\d+ .+\(\)): (.+)$' white,black,cyan bold,bold,normal

也就是说,第一个正则表达式组(parens)匹配日志文件中的初始日期,第二组匹配python文件名、行号和函数名,第三组匹配其后的日志消息。我还使用了“粗体/法线”的平行序列以及颜色序列。这看起来像:

请注意,不匹配任何正则表达式的行或行的一部分仍然被回显,因此这与'grep——color'不同——输出中没有过滤掉任何内容。

显然,这是足够灵活的,您可以将它用于任何进程,而不仅仅是跟踪日志文件。每当我想给一些东西上色时,我通常只是快速地创建一个新的正则表达式。出于这个原因,我更喜欢colout而不是任何自定义日志文件着色工具,因为我只需要学习一种工具,不管我在为什么着色:日志记录、测试输出、在终端中突出显示代码片段的语法等等。

它还避免了在日志文件本身中实际转储ANSI代码,恕我说这是一个坏主意,因为它会破坏日志文件中的grepping模式之类的事情,除非您始终记得在grep正则表达式中匹配ANSI代码。

其他回答

我已经知道了颜色转义,我使用他们在我的bash提示前一段时间。不管怎样,都要谢谢您。 我想要的是将它与日志模块集成在一起,经过几次尝试和错误后,我最终做到了这一点。 这是我最后得出的结论:

BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)

#The background is set with 40 plus the number of the color, and the foreground with 30

#These are the sequences need to get colored ouput
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"

def formatter_message(message, use_color = True):
    if use_color:
        message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
    else:
        message = message.replace("$RESET", "").replace("$BOLD", "")
    return message

COLORS = {
    'WARNING': YELLOW,
    'INFO': WHITE,
    'DEBUG': BLUE,
    'CRITICAL': YELLOW,
    'ERROR': RED
}

class ColoredFormatter(logging.Formatter):
    def __init__(self, msg, use_color = True):
        logging.Formatter.__init__(self, msg)
        self.use_color = use_color

    def format(self, record):
        levelname = record.levelname
        if self.use_color and levelname in COLORS:
            levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
            record.levelname = levelname_color
        return logging.Formatter.format(self, record)

要使用它,请创建自己的Logger:

# Custom logger class with multiple destinations
class ColoredLogger(logging.Logger):
    FORMAT = "[$BOLD%(name)-20s$RESET][%(levelname)-18s]  %(message)s ($BOLD%(filename)s$RESET:%(lineno)d)"
    COLOR_FORMAT = formatter_message(FORMAT, True)
    def __init__(self, name):
        logging.Logger.__init__(self, name, logging.DEBUG)                

        color_formatter = ColoredFormatter(self.COLOR_FORMAT)

        console = logging.StreamHandler()
        console.setFormatter(color_formatter)

        self.addHandler(console)
        return


logging.setLoggerClass(ColoredLogger)

以防有人需要。

Be careful if you're using more than one logger or handler: ColoredFormatter is changing the record object, which is passed further to other handlers or propagated to other loggers. If you have configured file loggers etc. you probably don't want to have the colors in the log files. To avoid that, it's probably best to simply create a copy of record with copy.copy() before manipulating the levelname attribute, or to reset the levelname to the previous value, before returning the formatted string (credit to Michael in the comments).

下面是一个适用于任何平台的解决方案。如果没有,告诉我,我会更新的。

它是如何工作的:在支持ANSI转义的平台上使用它们(非Windows),在Windows上它确实使用API调用来改变控制台颜色。

该脚本确实破解了标准库中的logging.StreamHandler.emit方法,并向其添加了包装器。

TestColorer.py

# Usage: add Colorer.py near you script and import it.
import logging
import Colorer

logging.warn("a warning")
logging.error("some error")
logging.info("some info")

Colorer.py

#!/usr/bin/env python
# encoding: utf-8
import logging
# now we patch Python code to add color support to logging.StreamHandler
def add_coloring_to_emit_windows(fn):
        # add methods we need to the class
    def _out_handle(self):
        import ctypes
        return ctypes.windll.kernel32.GetStdHandle(self.STD_OUTPUT_HANDLE)
    out_handle = property(_out_handle)

    def _set_color(self, code):
        import ctypes
        # Constants from the Windows API
        self.STD_OUTPUT_HANDLE = -11
        hdl = ctypes.windll.kernel32.GetStdHandle(self.STD_OUTPUT_HANDLE)
        ctypes.windll.kernel32.SetConsoleTextAttribute(hdl, code)

    setattr(logging.StreamHandler, '_set_color', _set_color)

    def new(*args):
        FOREGROUND_BLUE      = 0x0001 # text color contains blue.
        FOREGROUND_GREEN     = 0x0002 # text color contains green.
        FOREGROUND_RED       = 0x0004 # text color contains red.
        FOREGROUND_INTENSITY = 0x0008 # text color is intensified.
        FOREGROUND_WHITE     = FOREGROUND_BLUE|FOREGROUND_GREEN |FOREGROUND_RED
       # winbase.h
        STD_INPUT_HANDLE = -10
        STD_OUTPUT_HANDLE = -11
        STD_ERROR_HANDLE = -12

        # wincon.h
        FOREGROUND_BLACK     = 0x0000
        FOREGROUND_BLUE      = 0x0001
        FOREGROUND_GREEN     = 0x0002
        FOREGROUND_CYAN      = 0x0003
        FOREGROUND_RED       = 0x0004
        FOREGROUND_MAGENTA   = 0x0005
        FOREGROUND_YELLOW    = 0x0006
        FOREGROUND_GREY      = 0x0007
        FOREGROUND_INTENSITY = 0x0008 # foreground color is intensified.

        BACKGROUND_BLACK     = 0x0000
        BACKGROUND_BLUE      = 0x0010
        BACKGROUND_GREEN     = 0x0020
        BACKGROUND_CYAN      = 0x0030
        BACKGROUND_RED       = 0x0040
        BACKGROUND_MAGENTA   = 0x0050
        BACKGROUND_YELLOW    = 0x0060
        BACKGROUND_GREY      = 0x0070
        BACKGROUND_INTENSITY = 0x0080 # background color is intensified.     

        levelno = args[1].levelno
        if(levelno>=50):
            color = BACKGROUND_YELLOW | FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_INTENSITY 
        elif(levelno>=40):
            color = FOREGROUND_RED | FOREGROUND_INTENSITY
        elif(levelno>=30):
            color = FOREGROUND_YELLOW | FOREGROUND_INTENSITY
        elif(levelno>=20):
            color = FOREGROUND_GREEN
        elif(levelno>=10):
            color = FOREGROUND_MAGENTA
        else:
            color =  FOREGROUND_WHITE
        args[0]._set_color(color)

        ret = fn(*args)
        args[0]._set_color( FOREGROUND_WHITE )
        #print "after"
        return ret
    return new

def add_coloring_to_emit_ansi(fn):
    # add methods we need to the class
    def new(*args):
        levelno = args[1].levelno
        if(levelno>=50):
            color = '\x1b[31m' # red
        elif(levelno>=40):
            color = '\x1b[31m' # red
        elif(levelno>=30):
            color = '\x1b[33m' # yellow
        elif(levelno>=20):
            color = '\x1b[32m' # green 
        elif(levelno>=10):
            color = '\x1b[35m' # pink
        else:
            color = '\x1b[0m' # normal
        args[1].msg = color + args[1].msg +  '\x1b[0m'  # normal
        #print "after"
        return fn(*args)
    return new

import platform
if platform.system()=='Windows':
    # Windows does not support ANSI escapes and we are using API calls to set the console color
    logging.StreamHandler.emit = add_coloring_to_emit_windows(logging.StreamHandler.emit)
else:
    # all non-Windows platforms are supporting ANSI escapes so we use them
    logging.StreamHandler.emit = add_coloring_to_emit_ansi(logging.StreamHandler.emit)
    #log = logging.getLogger()
    #log.addFilter(log_filter())
    #//hdlr = logging.StreamHandler()
    #//hdlr.setFormatter(formatter())

一个方便的带有tput颜色的bash脚本

# Simple using tput
bold=$(tput bold)
reset=$(tput sgr0)

fblack=$(tput setaf 0)
fred=$(tput setaf 1)
fgreen=$(tput setaf 2)
fyellow=$(tput setaf 3)
fblue=$(tput setaf 4)
fmagenta=$(tput setaf 5)
fcyan=$(tput setaf 6)
fwhite=$(tput setaf 7)
fnotused=$(tput setaf 8)
freset=$(tput setaf 9)

bblack=$(tput setab 0)
bred=$(tput setab 1)
bgreen=$(tput setab 2)
byellow=$(tput setab 3)
bblue=$(tput setab 4)
bmagenta=$(tput setab 5)
bcyan=$(tput setab 6)
bwhite=$(tput setab 7)
bnotused=$(tput setab 8)
breset=$(tput setab 9)

# 0 - Emergency (emerg)       $fred       # something is wrong... go red
# 1 - Alerts (alert)          $fred       # something is wrong... go red
# 2 - Critical (crit)         $fred       # something is wrong... go red
# 3 - Errors (err)            $fred       # something is wrong... go red
# 4 - Warnings (warn)         $fyellow    # yellow yellow dirty logs
# 5 - Notification (notice)   $fwhite     # common stuff
# 6 - Information (info)      $fblue      # sky is blue
# 7 - Debug (debug)           $fgreen     # lot of stuff to read... go green 

我更喜欢使用这个片段:

import logging
from enum import Enum

CSI = '\033['

Color = Enum(
    'Color', 'BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE', start=30
)


class AnsiColorHandler(logging.StreamHandler):
    LOGLEVEL_COLORS = {
        'DEBUG': Color.BLUE,
        'INFO': Color.GREEN,
        'WARNING': Color.RED,
        'ERROR': Color.RED,
        'CRITICAL': Color.RED,
    }

    def __init__(self) -> None:
        super().__init__()
        self.formatter = logging.Formatter("%(levelname)-8s - %(message)s")

    def format(self, record: logging.LogRecord) -> str:
        message: str = super().format(record)
        # use colors in tty
        if self.stream.isatty() and (
            color := self.LOGLEVEL_COLORS.get(record.levelname)
        ):
            message = f'{CSI}{color.value}m{message}{CSI}0m'
        return message


# setup logger
# logger = logging.getLogger(__package__)
logger = logging.getLogger(__name__)
logger.addHandler(AnsiColorHandler())

用法:

import logging

from .log import logger

logger.setLevel(logging.DEBUG)
logger.debug("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")

这是一个包含颜色代码的Enum:

class TerminalColour:
    """
    Terminal colour formatting codes
    """
    # https://stackoverflow.com/questions/287871/print-in-terminal-with-colors
    MAGENTA = '\033[95m'
    BLUE = '\033[94m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    GREY = '\033[0m'  # normal
    WHITE = '\033[1m'  # bright white
    UNDERLINE = '\033[4m'

这可以应用于每个日志级别的名称。请注意,这是一个可怕的黑客。

logging.addLevelName(logging.INFO, "{}{}{}".format(TerminalColour.WHITE, logging.getLevelName(logging.INFO), TerminalColour.GREY))
logging.addLevelName(logging.WARNING, "{}{}{}".format(TerminalColour.YELLOW, logging.getLevelName(logging.WARNING), TerminalColour.GREY))
logging.addLevelName(logging.ERROR, "{}{}{}".format(TerminalColour.RED, logging.getLevelName(logging.ERROR), TerminalColour.GREY))
logging.addLevelName(logging.CRITICAL, "{}{}{}".format(TerminalColour.MAGENTA, logging.getLevelName(logging.CRITICAL), .GREY))

注意,您的日志格式化程序必须包含日志级别的名称

%(levelname)

例如:

    LOGGING = {
...
        'verbose': {
            'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)s %(module)s %(process)d %(thread)d %(message)s'
        },
        'simple': {
            'format': '[%(asctime)s] %(levelname)s %(name)s %(message)s'
        },