Como adiciono um campo personalizado à string de formato de registro do Python?

96

Minha string de formato atual é:

formatter = logging.Formatter('%(asctime)s : %(message)s')

e desejo adicionar um novo campo chamado, app_nameque terá um valor diferente em cada script que contém esse formatador.

import logging
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.addHandler(syslog)

Mas não tenho certeza de como passar esse app_namevalor para o logger para interpolar na string de formato. Obviamente, posso fazer com que apareça na mensagem de log passando-o todas as vezes, mas isso é confuso.

Eu tentei:

logging.info('Log message', app_name='myapp')
logging.info('Log message', {'app_name', 'myapp'})
logging.info('Log message', 'myapp')

mas nenhum funciona.

apelido
fonte
Você realmente deseja passar isso para todas as logchamadas? Em caso afirmativo, olhe para os documentos onde diz "Esta funcionalidade pode ser usada para injetar seus próprios valores em um LogRecord ..." Mas este parece ser o caso principal para usar logger = logging.getLogger('myapp')e incluí- lo na logger.infochamada.
abarnert em
o python logging já pode fazer isso. se você usar um diferente loggerobjeto em cada aplicativo, você pode fazer cada um usar um nome diferente por instanciar seus loggers assim: logger = logging.getLogger(myAppName). observe que __name__é o nome do módulo python, portanto, se cada aplicativo for seu próprio módulo python, isso também funcionaria.
Florian Castellane

Respostas:

135

Você poderia usar um LoggerAdapter para não ter que passar as informações extras a cada chamada de registro:

import logging
extra = {'app_name':'Super App'}

logger = logging.getLogger(__name__)
syslog = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(syslog)

logger = logging.LoggerAdapter(logger, extra)
logger.info('The sky is so blue')

logs (algo como)

2013-07-09 17:39:33,596 Super App : The sky is so blue

Os filtros também podem ser usados ​​para adicionar informações contextuais.

import logging

class AppFilter(logging.Filter):
    def filter(self, record):
        record.app_name = 'Super App'
        return True

logger = logging.getLogger(__name__)
logger.addFilter(AppFilter())
syslog = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(app_name)s : %(message)s')
syslog.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addHandler(syslog)

logger.info('The sky is so blue')

produz um registro de log semelhante.

unutbu
fonte
3
Como podemos especificar isso em um config.iniarquivo? Desejo adicionar o nome do host atual socket.gethostname().
Laurent LAPORTE
Eu tenho esta amostra que não funciona para mim. import uuid uniqueId = str(uuid.uuid4()) extra = {"u_id" : uniqueId} RotatingHandler = RotatingFileHandler(LOG_FILENAME,encoding='utf-8',maxBytes=maxSize, backupCount=batchSize) logger.basicConfig(handlers=[RotatingHandler],level=logLevel.upper(),format='%(levelname)s %(u_id)s %(funcName)s %(asctime)s %(message)s ',datefmt='%m/%d/%Y %I:%M:%S %p') logger = logger.LoggerAdapter(logger=logger, extra=extra)
Hayat,
É possível adicionar um campo "level" que seja igual a "levelname"? Consulte: Como posso renomear “levelname” para “level” nas mensagens de log do Python?
Martin Thoma,
2
Posso apenas passar uma sequência de informações extras. Algo assim: "Ocorreu um erro para o ID do funcionário 1029382" Sem criar nenhum dicionário.
shreesh katti
51

Você precisa passar o dict como um parâmetro para extra para fazer isso dessa maneira.

logging.info('Log message', extra={'app_name': 'myapp'})

Prova:

>>> import logging
>>> logging.basicConfig(format="%(foo)s - %(message)s")
>>> logging.warning('test', extra={'foo': 'bar'})
bar - test 

Além disso, como observação, se você tentar registrar uma mensagem sem passar o dict, ela falhará.

>>> logging.warning('test')
Traceback (most recent call last):
  File "/usr/lib/python2.7/logging/__init__.py", line 846, in emit
    msg = self.format(record)
  File "/usr/lib/python2.7/logging/__init__.py", line 723, in format
    return fmt.format(record)
  File "/usr/lib/python2.7/logging/__init__.py", line 467, in format
    s = self._fmt % record.__dict__
KeyError: 'foo'
Logged from file <stdin>, line 1
mr2ert
fonte
Isso também funcionará para logging.info()? Ele falhou na última vez que tentei. : /
Prakhar Mohan Srivastava
2
Eu gosto da resposta de @ mr2ert. Você pode fornecer um valor padrão para o campo extra estendendo a logging.Formatterclasse: class CustomFormatter (logging.Formatter): def formato (self, record): if not hasattr (record, 'foo'): record.foo = 'default_foo' return super (CustomFormatter, self.format (record) h = loggin.StreamHandler () h.setFormatter (CustomFormatter ('% (foo) s% (message) s') logger = logging.getLogger ('bar') logger.addHandler ( h) logger.error ('hey!', extra = {'foo': 'FOO'}) logger.error ('hey!')
loutre
Esse método é mais rápido, mas você precisa adicionar linhas extras em cada mensagem de log, que são fáceis de esquecer e sujeitas a erros. Substituir as super () chamadas é mais confuso do que atender de unutbu.
pevogam
@Prakhar Mohan Srivastava Sim. Funcionará bem para logging.info () também. Qual mensagem de erro você está recebendo ?
shreesh katti
Posso apenas passar uma sequência de informações extras. Algo assim: "Ocorreu um erro para o ID do funcionário 1029382" Sem criar nenhum dicionário e passar as chaves
shreesh katti
25

Python3

A partir do Python 3.2, você agora pode usar LogRecordFactory

>>> import logging
>>> logging.basicConfig(format="%(custom_attribute)s - %(message)s")
>>> old_factory = logging.getLogRecordFactory()
>>> def record_factory(*args, **kwargs):
        record = old_factory(*args, **kwargs)
        record.custom_attribute = "my-attr"
        return record

>>> logging.setLogRecordFactory(record_factory)
>>> logging.info("hello")
my-attr - hello

É claro que record_factorypode ser personalizado para qualquer chamada e o valor de custom_attributepode ser atualizado se você mantiver uma referência para a chamada de fábrica.

Por que isso é melhor do que usar adaptadores / filtros?

  • Você não precisa passar seu logger pelo aplicativo
  • Na verdade, ele funciona com bibliotecas de terceiros que usam seu próprio logger (apenas chamando logger = logging.getLogger(..)) e agora têm o mesmo formato de log. (este não é o caso com Filtros / Adaptadores onde você precisa usar o mesmo objeto logger)
  • Você pode empilhar / encadear várias fábricas
Ahmad
fonte
Existe alguma alternativa para o python 2.7?
Karolch
Não com os mesmos benefícios, com 2.7 você teria que ir com adaptadores ou filtros.
Ahmad
5
Esta é a melhor resposta do python3 de hoje
Stéphane
De acordo com docs.python.org/3/howto/logging-cookbook.html : Este padrão permite que diferentes bibliotecas encadear fábricas juntas, e desde que não substituam os atributos umas das outras ou involuntariamente substituam os atributos fornecidos como padrão, há não deve haver surpresas. No entanto, deve-se ter em mente que cada elo da cadeia adiciona sobrecarga de tempo de execução a todas as operações de registro, e a técnica só deve ser usada quando o uso de um Filtro não fornece o resultado desejado.
steve0hh
1
@ steve0hh um dos principais resultados desejados é a capacidade de registrar informações contextuais em diferentes bibliotecas / módulos, o que só poderia ser obtido dessa forma. Na maioria das circunstâncias, as bibliotecas não devem tocar na configuração do criador de logs, é responsabilidade do aplicativo pai.
Ahmad de
10

Outra maneira é criar um LoggerAdapter personalizado. Isso é particularmente útil quando você não pode alterar o formato OU se o seu formato é compartilhado com um código que não envia a chave exclusiva (no seu caso app_name ):

class LoggerAdapter(logging.LoggerAdapter):
    def __init__(self, logger, prefix):
        super(LoggerAdapter, self).__init__(logger, {})
        self.prefix = prefix

    def process(self, msg, kwargs):
        return '[%s] %s' % (self.prefix, msg), kwargs

E em seu código, você criaria e inicializaria seu registrador como de costume:

    logger = logging.getLogger(__name__)
    # Add any custom handlers, formatters for this logger
    myHandler = logging.StreamHandler()
    myFormatter = logging.Formatter('%(asctime)s %(message)s')
    myHandler.setFormatter(myFormatter)
    logger.addHandler(myHandler)
    logger.setLevel(logging.INFO)

Finalmente, você criaria o adaptador de wrapper para adicionar um prefixo conforme necessário:

    logger = LoggerAdapter(logger, 'myapp')
    logger.info('The world bores you when you are cool.')

O resultado será mais ou menos assim:

2013-07-09 17:39:33,596 [myapp] The world bores you when you are cool.
rublo
fonte
1

Eu encontrei esta pergunta SO depois de implementá-la sozinho. Espero que ajude alguém. No código abaixo, estou induzindo uma chave extra chamada claim_idno formato logger. Ele registrará o Claim_id sempre que houver uma claim_idchave presente no ambiente. No meu caso de uso, precisei registrar essas informações para uma função do AWS Lambda.

import logging
import os

LOG_FORMAT = '%(asctime)s %(name)s %(levelname)s %(funcName)s %(lineno)s ClaimID: %(claim_id)s: %(message)s'


class AppLogger(logging.Logger):

    # Override all levels similarly - only info overriden here

    def info(self, msg, *args, **kwargs):
        return super(AppLogger, self).info(msg, extra={"claim_id": os.getenv("claim_id", "")})


def get_logger(name):
    """ This function sets log level and log format and then returns the instance of logger"""
    logging.setLoggerClass(AppLogger)
    logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    return logger


LOGGER = get_logger(__name__)

LOGGER.info("Hey")
os.environ["claim_id"] = "12334"
LOGGER.info("Hey")

Síntese: https://gist.github.com/ramanujam/306f2e4e1506f302504fb67abef50652

Rhn89
fonte
1

A resposta aceita não registrou o formato no arquivo de log, enquanto o formato foi refletido na saída sys. Como alternativa, usei uma abordagem mais simples e trabalhei como;

logging.basicConfig(filename="mylogfile.test",
                    filemode="w+",
                    format='%(asctime)s: ' +app_name+': %(message)s ',
                    level=logging.DEBUG)

PriyaGp
fonte
0

Usando a resposta de mr2ert, eu vim com esta solução confortável (embora eu ache que não seja recomendada) - Substitua os métodos de registro integrados para aceitar o argumento personalizado e criar o extradicionário dentro dos métodos:

import logging

class CustomLogger(logging.Logger):

   def debug(self, msg, foo, *args, **kwargs):
       extra = {'foo': foo}

       if self.isEnabledFor(logging.DEBUG):
            self._log(logging.DEBUG, msg, args, extra=extra, **kwargs)

   *repeat for info, warning, etc*

logger = CustomLogger('CustomLogger', logging.DEBUG)
formatter = logging.Formatter('%(asctime)s [%(foo)s] %(message)s') 
handler = logging.StreamHandler()
handler.setFormatter(formatter) 
logger.addHandler(handler)

logger.debug('test', 'bar')

Resultado:

2019-03-02 20:06:51,998 [bar] test

Esta é a função integrada para referência:

def debug(self, msg, *args, **kwargs):
    """
    Log 'msg % args' with severity 'DEBUG'.

    To pass exception information, use the keyword argument exc_info with
    a true value, e.g.

    logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
    """
    if self.isEnabledFor(DEBUG):
        self._log(DEBUG, msg, args, **kwargs)
Yaniv K.
fonte
0

registro de importação;

classe LogFilter (logging.Filter):

def __init__(self, code):
    self.code = code

def filter(self, record):
    record.app_code = self.code
    return True

logging.basicConfig (format = '[% (asctime) s:% (levelname) s] :: [% (module) s ->% (name) s] - APP_CODE:% (app_code) s - MSG:% (mensagem ) s ');

classe Logger:

def __init__(self, className):
    self.logger = logging.getLogger(className)
    self.logger.setLevel(logging.ERROR)

@staticmethod
def getLogger(className):
    return Logger(className)

def logMessage(self, level, code, msg):
    self.logger.addFilter(LogFilter(code))

    if level == 'WARN':        
        self.logger.warning(msg)
    elif level == 'ERROR':
        self.logger.error(msg)
    else:
        self.logger.info(msg)

classe Teste: logger = Logger.getLogger ('Teste')

if __name__=='__main__':
    logger.logMessage('ERROR','123','This is an error')
user3672617
fonte
Esta implementação será muito ineficiente.
blakev