¿Cómo agrego un campo personalizado a la cadena de formato de registro de Python?

91

Mi cadena de formato actual es:

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

y quiero agregar un nuevo campo llamado app_nameque tendrá un valor diferente en cada script que contiene este formateador.

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

Pero no estoy seguro de cómo pasar ese app_namevalor al registrador para interpolarlo en la cadena de formato. Obviamente, puedo hacer que aparezca en el mensaje de registro pasándolo cada vez, pero esto es complicado.

He intentado:

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

pero ninguno funciona.

nickponline
fuente
¿Realmente quieres pasar esto a cada logllamada? Si es así, mire los documentos donde dice "Esta funcionalidad puede usarse para inyectar sus propios valores en un LogRecord ..." Pero este parece ser un caso excelente para usarlo logger = logging.getLogger('myapp')y tenerlo integrado en la logger.infollamada.
abarnert
el registro de Python ya puede hacer eso afaik. si se utiliza un diferente loggerobjeto en cada aplicación, puede hacer que cada uno de los usos un nombre diferente creando instancias de sus loggers de este modo: logger = logging.getLogger(myAppName). tenga en cuenta que __name__es el nombre del módulo de Python, por lo que si cada aplicación es su propio módulo de Python, eso también funcionaría.
Florian Castellane

Respuestas:

131

Puede usar un LoggerAdapter para no tener que pasar la información adicional con cada llamada 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')

registros (algo así como)

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

Los filtros también se pueden utilizar para agregar información contextual.

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

produce un registro de registro similar.

unutbu
fuente
3
¿Cómo podemos especificar eso en un config.iniarchivo? Deseo agregar el nombre de host actual socket.gethostname().
Laurent LAPORTE
Tengo esta muestra que no me funciona. 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
¿Es posible agregar un campo "nivel" que sea igual a "nombre de nivel"? Consulte: ¿Cómo puedo cambiar el nombre de "levelname" a "level" en los mensajes de registro de Python?
Martin Thoma
2
¿Puedo pasar una serie de información adicional? Algo como esto: "Se produjo un error para el ID de empleado 1029382" Sin crear ningún diccionario.
shreesh katti
50

Necesita pasar el dict como parámetro a extra para hacerlo de esa manera.

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

Prueba:

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

Además, como nota, si intenta registrar un mensaje sin pasar el dict, fallará.

>>> 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
fuente
¿Funcionará esto también logging.info()? Falló cuando lo intenté por última vez. : /
Prakhar Mohan Srivastava
2
Me gusta la respuesta de @ mr2ert. Puede dar un valor predeterminado al campo adicional extendiendo la logging.Formatterclase: class CustomFormatter (logging.Formatter): def format (self, record): if not hasattr (record, 'foo'): record.foo = 'default_foo' return super (CustomFormatter, self.format (registro) 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
Este método es más rápido, pero debe agregar líneas adicionales en cada mensaje de registro, lo cual es fácil de olvidar y propenso a errores. Reemplazar las llamadas super () es más complicado que la respuesta de unutbu.
pevogam
@Prakhar Mohan Srivastava Sí. También funcionará bien para logging.info (). ¿Qué mensaje de error está recibiendo?
shreesh katti
¿Puedo pasar una serie de información adicional? Algo como esto: "Se produjo un error para el ID de empleado 1029382" Sin crear ningún diccionario y pasar las claves
shreesh katti
23

Python3

A partir de Python3.2 ahora puede 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

Por supuesto, record_factoryse puede personalizar para que sea invocable y el valor de custom_attributepodría actualizarse si mantiene una referencia al invocable de fábrica.

¿Por qué es mejor que usar adaptadores / filtros?

  • No necesita pasar su registrador por la aplicación
  • En realidad, funciona con bibliotecas de terceros que usan su propio registrador (con solo llamar logger = logging.getLogger(..)) y ahora tendrán el mismo formato de registro. (este no es el caso con Filtros / Adaptadores donde necesita usar el mismo objeto de registrador)
  • Puede apilar / encadenar varias fábricas
Ahmad
fuente
¿Hay alguna alternativa para Python 2.7?
karolch
No con los mismos beneficios, con 2.7 tendrías que ir con adaptadores o filtros.
Ahmad
5
Esta es la mejor respuesta actual de python3
Stéphane
De acuerdo con docs.python.org/3/howto/logging-cookbook.html : este patrón permite que diferentes bibliotecas encadenen fábricas juntas, y siempre que no sobrescriban los atributos de las otras o sobrescriban involuntariamente los atributos proporcionados como estándar, no No debería haber sorpresas. Sin embargo, debe tenerse en cuenta que cada eslabón de la cadena agrega una sobrecarga de tiempo de ejecución a todas las operaciones de registro, y la técnica solo debe usarse cuando el uso de un filtro no proporciona el resultado deseado.
steve0hh
1
@ steve0hh uno de los resultados clave deseados es la capacidad de registrar información contextual en diferentes bibliotecas / módulos, lo que solo podría lograrse de esta manera. En la mayoría de las circunstancias, las bibliotecas no deberían tocar la configuración del registrador, es responsabilidad de la aplicación principal.
Ahmad
9

Otra forma es crear un LoggerAdapter personalizado. Esto es particularmente útil cuando no puede cambiar el formato O si su formato se comparte con un código que no envía la clave única (en su 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

Y en su código, crearía e inicializaría su registrador como de costumbre:

    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, crearía el adaptador de contenedor para agregar un prefijo según sea necesario:

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

La salida se verá así:

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

Encontré esta pregunta SO después de implementarla yo mismo. Espero que ayude a alguien. En el siguiente código, estoy induciendo una clave adicional llamada claim_iden formato de registrador. Registrará el Claim_id siempre que haya una claim_idclave presente en el entorno. En mi caso de uso, necesitaba registrar esta información para una función de 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")

Gist: https://gist.github.com/ramanujam/306f2e4e1506f302504fb67abef50652

rhn89
fuente
0

Usando la respuesta de mr2ert, se me ocurrió esta solución cómoda (aunque supongo que no es recomendable): anule los métodos de registro integrados para aceptar el argumento personalizado y crear el extradiccionario dentro de los 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')

Salida:

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

Esta es la función incorporada para referencia:

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.
fuente
0

registro de importación;

clase 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:% (mensaje ) s ');

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

clase Prueba: logger = Logger.getLogger ('Prueba')

if __name__=='__main__':
    logger.logMessage('ERROR','123','This is an error')
user3672617
fuente
Esta implementación será muy ineficiente.
blakev