tempo - python logging filters




Como posso colorir a saída de log do Python? (16)

Agora há um módulo PyPi lançado para saída de log colorido personalizável:

https://pypi.python.org/pypi/rainbow_logging_handler/

e

https://github.com/laysakura/rainbow_logging_handler

  • Suporta Windows

  • Suporta Django

  • Cores personalizáveis

Como isso é distribuído como um ovo Python, é muito fácil de instalar para qualquer aplicativo Python.

Algum tempo atrás, eu vi um aplicativo Mono com saída colorida, presumivelmente por causa de seu sistema de log (porque todas as mensagens foram padronizadas).

Agora, o Python tem o módulo de logging , que permite especificar muitas opções para personalizar a saída. Então, estou imaginando que algo semelhante seria possível com o Python, mas não consigo descobrir como fazer isso em qualquer lugar.

Existe alguma maneira de tornar o módulo de logging do Python em cores?

O que eu quero (por exemplo) erros em vermelho, depurar mensagens em azul ou amarelo e assim por diante.

É claro que isso provavelmente exigiria um terminal compatível (a maioria dos terminais modernos é); mas eu poderia voltar para a saída de logging original se a cor não for suportada.

Alguma idéia de como eu posso obter saída colorida com o módulo de registro?


Anos atrás eu escrevi um manipulador de fluxo colorido para meu próprio uso. Então me deparei com esta página e encontrei uma coleção de trechos de código que as pessoas estão copiando / colando :-(. Meu manipulador de fluxo atualmente só funciona no UNIX (Linux, Mac OS X) mas a vantagem é que ele está disponível no PyPI (e GitHub ) e é muito simples de usar, também tem um modo de sintaxe Vim :-). No futuro, posso estendê-lo para funcionar no Windows.

Para instalar o pacote:

$ pip install coloredlogs

Para confirmar que funciona:

$ coloredlogs --demo

Para começar com seu próprio código:

$ python
> import coloredlogs, logging
> coloredlogs.install()
> logging.info("It works!")
2014-07-30 21:21:26 peter-macbook root[7471] INFO It works!

O formato de log padrão mostrado no exemplo acima contém a data, hora, nome do host, o nome do logger, o PID, o nível de log e a mensagem de log. Isto é o que parece na prática:


Aqui está uma solução que deve funcionar em qualquer plataforma. Se isso não apenas me disser e eu vou atualizá-lo.

Como funciona: em plataformas que suportam escapes ANSI está usando-as (não-Windows) e no Windows ele usa chamadas de API para mudar as cores do console.

O script corta o método logging.StreamHandler.emit da biblioteca padrão, adicionando um wrapper a ele.

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

Bem, acho que também posso adicionar minha variação do logger colorido.

Isso não é nada chique, mas é muito simples de usar e não altera o objeto de registro, evitando assim o registro das seqüências de escape ANSI em um arquivo de log, se um manipulador de arquivos for usado. Isso não afeta a formatação da mensagem de log.

Se você já estiver usando o formatador do módulo de registro , tudo o que precisa fazer para obter nomes de nível coloridos é substituir o formatador de manipuladores de conselhos pelo ColorFormatter. Se você está registrando um aplicativo inteiro, você só precisa fazer isso para o logger de nível superior.

colored_log.py

#!/usr/bin/env python

from copy import copy
from logging import Formatter

MAPPING = {
    'DEBUG'   : 37, # white
    'INFO'    : 36, # cyan
    'WARNING' : 33, # yellow
    'ERROR'   : 31, # red
    'CRITICAL': 41, # white on red bg
}

PREFIX = '\033['
SUFFIX = '\033[0m'

class ColoredFormatter(Formatter):

    def __init__(self, patern):
        Formatter.__init__(self, patern)

    def format(self, record):
        colored_record = copy(record)
        levelname = colored_record.levelname
        seq = MAPPING.get(levelname, 37) # default white
        colored_levelname = ('{0}{1}m{2}{3}') \
            .format(PREFIX, seq, levelname, SUFFIX)
        colored_record.levelname = colored_levelname
        return Formatter.format(self, colored_record)

Exemplo de uso

app.py

#!/usr/bin/env python

import logging
from colored_log import ColoredFormatter

# Create top level logger
log = logging.getLogger("main")

# Add console handler using our custom ColoredFormatter
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
cf = ColoredFormatter("[%(name)s][%(levelname)s]  %(message)s (%(filename)s:%(lineno)d)")
ch.setFormatter(cf)
log.addHandler(ch)

# Add file handler
fh = logging.FileHandler('app.log')
fh.setLevel(logging.DEBUG)
ff = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(ff)
log.addHandler(fh)

# Set log level
log.setLevel(logging.DEBUG)

# Log some stuff
log.debug("app has started")
log.info("Logging to 'app.log' in the script dir")
log.warning("This is my last warning, take heed")
log.error("This is an error")
log.critical("He's dead, Jim")

# Import a sub-module 
import sub_module

sub_module.py

#!/usr/bin/env python

import logging
log = logging.getLogger('main.sub_module')

log.debug("Hello from the sub module")

Resultados

Saída terminal

conteúdo do app.log

2017-09-29 00:32:23,434 - main - DEBUG - app has started
2017-09-29 00:32:23,434 - main - INFO - Logging to 'app.log' in the script dir
2017-09-29 00:32:23,435 - main - WARNING - This is my last warning, take heed
2017-09-29 00:32:23,435 - main - ERROR - This is an error
2017-09-29 00:32:23,435 - main - CRITICAL - He's dead, Jim
2017-09-29 00:32:23,435 - main.sub_module - DEBUG - Hello from the sub module

É claro que você pode obter o máximo que quiser com a formatação das saídas do terminal e do arquivo de log. Apenas o nível de log será colorido.

Espero que alguém ache isso útil e não é muito mais do mesmo. :)

Os arquivos de exemplo do Python podem ser baixados deste GitHub Gist: https://gist.github.com/KurtJacobson/48e750701acec40c7161b5a2f79e6bfd


Eu já sabia sobre as fugas de cores, usei-as no meu prompt bash há um tempo atrás. Obrigado mesmo assim.
O que eu queria era integrá-lo com o módulo de registro, o que acabei fazendo depois de algumas tentativas e erros.
Aqui está o que eu acabo com:

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)

E para usá-lo, crie seu próprio 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)

Apenas no caso de alguém mais precisar.

Tenha cuidado se você estiver usando mais de um logger ou manipulador: O ColoredFormatter está alterando o objeto de registro, que é passado adiante para outros manipuladores ou propagado para outros registradores. Se você configurou loggers de arquivos, etc., provavelmente não deseja ter as cores nos arquivos de log. Para evitar isso, provavelmente é melhor simplesmente criar uma cópia do record com copy.copy() antes de manipular o atributo levelname, ou redefinir o nome do nível para o valor anterior, antes de retornar a string formatada (crédito para Michael nos comentários).


Eu modifiquei o exemplo original fornecido por Sorin e subclasse o StreamHandler para um ColorizedConsoleHandler.

A desvantagem de sua solução é que ela modifica a mensagem e, como isso está modificando o logmessage real, qualquer outro manipulador receberá também a mensagem modificada.

Isso resultou em arquivos de log com códigos de cores no nosso caso, porque usamos vários registradores.

A classe abaixo só funciona em plataformas que suportam ansi, mas deve ser trivial adicionar os códigos de cor do Windows a ela.

import copy
import logging


class ColoredConsoleHandler(logging.StreamHandler):
    def emit(self, record):
        # Need to make a actual copy of the record
        # to prevent altering the message for other loggers
        myrecord = copy.copy(record)
        levelno = myrecord.levelno
        if(levelno >= 50):  # CRITICAL / FATAL
            color = '\x1b[31m'  # red
        elif(levelno >= 40):  # ERROR
            color = '\x1b[31m'  # red
        elif(levelno >= 30):  # WARNING
            color = '\x1b[33m'  # yellow
        elif(levelno >= 20):  # INFO
            color = '\x1b[32m'  # green
        elif(levelno >= 10):  # DEBUG
            color = '\x1b[35m'  # pink
        else:  # NOTSET and anything else
            color = '\x1b[0m'  # normal
        myrecord.msg = color + str(myrecord.msg) + '\x1b[0m'  # normal
        logging.StreamHandler.emit(self, myrecord)

O pouco que eu tive problema foi configurar o formatador corretamente:

class ColouredFormatter(logging.Formatter):    
    def __init__(self, msg):
        logging.Formatter.__init__(self, msg)
        self._init_colour = _get_colour()

    def close(self):
        # restore the colour information to what it was
        _set_colour(self._init_colour)

    def format(self, record):        
        # Add your own colourer based on the other examples
        _set_colour( LOG_LEVEL_COLOUR[record.levelno] )
        return logging.Formatter.format(self, record)         

def init():
    # Set up the formatter. Needs to be first thing done.
    rootLogger = logging.getLogger()
    hdlr = logging.StreamHandler()
    fmt = ColouredFormatter('%(message)s')
    hdlr.setFormatter(fmt)
    rootLogger.addHandler(hdlr)

E então para usar:

import coloured_log
import logging

coloured_log.init()
logging.info("info")    
logging.debug("debug")    

coloured_log.close()    # restore colours

Outro remix menor da abordagem do airmind que mantém tudo em uma classe:

class ColorFormatter(logging.Formatter):
  FORMAT = ("[$BOLD%(name)-20s$RESET][%(levelname)-18s]  "
            "%(message)s "
            "($BOLD%(filename)s$RESET:%(lineno)d)")

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

  RESET_SEQ = "\033[0m"
  COLOR_SEQ = "\033[1;%dm"
  BOLD_SEQ = "\033[1m"

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

  def formatter_msg(self, msg, use_color = True):
    if use_color:
      msg = msg.replace("$RESET", self.RESET_SEQ).replace("$BOLD", self.BOLD_SEQ)
    else:
      msg = msg.replace("$RESET", "").replace("$BOLD", "")
    return msg

  def __init__(self, use_color=True):
    msg = self.formatter_msg(self.FORMAT, use_color)
    logging.Formatter.__init__(self, msg)
    self.use_color = use_color

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

Para usar anexar o formatador a um manipulador, algo como:

handler.setFormatter(ColorFormatter())
logger.addHandler(handler)

Uma ferramenta simples mas muito flexível para colorir QUALQUER texto de terminal é ' colout '.

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

Onde qualquer texto na saída de 'myprocess' que corresponda ao grupo 1 da regex será colorido com color1, group 2 com color2, etc.

Por exemplo:

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

ou seja, o primeiro grupo regex (parens) corresponde à data inicial no arquivo de log, o segundo grupo corresponde a um nome de arquivo python, número de linha e nome da função, e o terceiro grupo corresponde à mensagem de log que vem depois disso. Eu também uso uma seqüência paralela de 'negrito / normal' bem como a seqüência de cores. Isso parece:

Note que as linhas ou partes de linhas que não correspondem a nenhuma da minha regex ainda são ecoadas, então isso não é como 'grep --color' - nada é filtrado da saída.

Obviamente, isso é flexível o suficiente para que você possa usá-lo com qualquer processo, não apenas com arquivos de log. Eu costumo apenas preparar um novo regex imediatamente toda vez que eu quero colorir algo. Por esta razão, eu prefiro colout para qualquer ferramenta personalizada de coloração de arquivo de log, porque eu só preciso aprender uma ferramenta, independentemente do que eu estou colorindo: registro, saída de teste, realce de sintaxe de trechos de código no terminal, etc.

Ele também evita realmente despejar códigos ANSI no arquivo de log em si, o que IMHO é uma má idéia, porque vai quebrar coisas como grepping para padrões no logfile a menos que você sempre lembre de combinar os códigos ANSI em seu regex grep.



Atualização : Como essa é uma coceira que eu queria arranhar por tanto tempo, fui em frente e escrevi uma biblioteca para pessoas preguiçosas como eu, que só querem maneiras simples de fazer as coisas: zenlog

O Colorlog é excelente para isso. Ele está disponível no PyPI (e, portanto, pode ser instalado através do pip install colorlog ) e é mantido ativamente .

Aqui está um pequeno trecho de cópia e pastilha para configurar o log e imprimir mensagens de log de aparência decente:

import logging
LOG_LEVEL = logging.DEBUG
LOGFORMAT = "  %(log_color)s%(levelname)-8s%(reset)s | %(log_color)s%(message)s%(reset)s"
from colorlog import ColoredFormatter
logging.root.setLevel(LOG_LEVEL)
formatter = ColoredFormatter(LOGFORMAT)
stream = logging.StreamHandler()
stream.setLevel(LOG_LEVEL)
stream.setFormatter(formatter)
log = logging.getLogger('pythonConfig')
log.setLevel(LOG_LEVEL)
log.addHandler(stream)

log.debug("A quirky message only developers care about")
log.info("Curious users might want to know this")
log.warn("Something is wrong and any user should be informed")
log.error("Serious stuff, this is red for a reason")
log.critical("OH NO everything is on fire")

Saída:


Apenas mais uma solução, com as cores do ZetaSyanthis:

def config_log(log_level):

    def set_color(level, code):
        level_fmt = "\033[1;" + str(code) + "m%s\033[1;0m" 
        logging.addLevelName( level, level_fmt % logging.getLevelName(level) )

    std_stream = sys.stdout
    isatty = getattr(std_stream, 'isatty', None)
    if isatty and isatty():
        levels = [logging.DEBUG, logging.CRITICAL, logging.WARNING, logging.ERROR]
        for idx, level in enumerate(levels):
            set_color(level, 30 + idx )
        set_color(logging.DEBUG, 0)
    logging.basicConfig(stream=std_stream, level=log_level)

chame-o uma vez da sua __main__função. Eu tenho algo assim aqui:

options, arguments = p.parse_args()
log_level = logging.DEBUG if options.verbose else logging.WARNING
config_log(log_level)

ele também verifica se a saída é um console, caso contrário, nenhuma cor será usada.



Enquanto as outras soluções parecem bem, elas têm alguns problemas. Alguns colorem as linhas inteiras que algumas vezes não são desejadas e algumas omitem qualquer configuração que você possa ter em conjunto. A solução abaixo não afeta nada além da própria mensagem.

Código

class ColoredFormatter(logging.Formatter):
    def format(self, record):
        if record.levelno == logging.WARNING:
            record.msg = '\033[93m%s\033[0m' % record.msg
        elif record.levelno == logging.ERROR:
            record.msg = '\033[91m%s\033[0m' % record.msg
        return logging.Formatter.format(self, record)

Exemplo

logger = logging.getLogger('mylogger')
handler = logging.StreamHandler()

log_format = '[%(asctime)s]:%(levelname)-7s:%(message)s'
time_format = '%H:%M:%S'
formatter = ColoredFormatter(log_format, datefmt=time_format)
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.warn('this should be yellow')
logger.error('this should be red')

Saída

[17:01:36]:WARNING:this should be yellow
[17:01:37]:ERROR  :this should be red

Como você vê, todo o resto ainda é produzido e permanece na cor inicial. Se você quiser alterar qualquer outra coisa que não seja a mensagem, simplesmente passe os códigos de cor para log_formato exemplo.


Use pyfancy .

Exemplo:

print(pyfancy.RED + "Hello Red!" + pyfancy.END)

import logging

logging.basicConfig(filename="f.log" filemode='w', level=logging.INFO,
                    format = "%(logger_name)s %(color)s  %(message)s %(endColor)s")


class Logger(object):
    __GREEN = "\033[92m"
    __RED = '\033[91m'
    __ENDC = '\033[0m'

    def __init__(self, name):
        self.logger = logging.getLogger(name)
        self.extra={'logger_name': name, 'endColor': self.__ENDC, 'color': self.__GREEN}


    def info(self, msg):
        self.extra['color'] = self.__GREEN
        self.logger.info(msg, extra=self.extra)

    def error(self, msg):
        self.extra['color'] = self.__RED
        self.logger.error(msg, extra=self.extra)

Uso

Logger("File Name").info("This shows green text")





colors