【问题标题】:How do I add custom field to Python log format string?如何将自定义字段添加到 Python 日志格式字符串?
【发布时间】:2022-01-19 10:08:49
【问题描述】:

我当前的格式字符串是:

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

我想添加一个名为 app_name 的新字段,它在包含此格式化程序的每个脚本中都有不同的值。

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

但我不确定如何将 app_name 值传递给记录器以插入到格式字符串中。我显然可以通过每次传递它来让它出现在日志消息中,但这很混乱。

我试过了:

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

但没有任何作用。

【问题讨论】:

  • 你真的想把这个传递给每个log 电话吗?如果是这样,请查看 the docs,它说“此功能可用于将您自己的值注入 LogRecord……”但这似乎是使用 logger = logging.getLogger('myapp') 并将其烘焙到 logger.info 调用中的主要案例。
  • python 日志记录已经可以做到这一点。如果您在每个应用程序中使用不同的logger 对象,您可以通过实例化您的loggers 使每个对象使用不同的名称,如下所示:logger = logging.getLogger(myAppName)。请注意,__name__ 是 python 模块名称,所以如果每个应用程序都是它自己的 python 模块,那也可以。

标签: python logging


【解决方案1】:

您需要将 dict 作为参数传递给 extra 才能这样做。

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

证明:

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

另外,请注意,如果您尝试在不传递字典的情况下记录消息,那么它将失败。

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

【讨论】:

  • 这也适用于logging.info() 吗?我上次尝试时失败了。 ://
  • 我喜欢@mr2ert 的回答。您可以通过扩展 logging.Formatter 类为额外字段提供默认值: class CustomFormatter(logging.Formatter): def format(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!')
  • 这种方式比较快,但是需要在每条日志信息上多加几行,容易忘记,容易出错。替换 super() 调用比 unutbu 的答案更混乱。
  • @Prakhar Mohan Srivastava 是的,它也适用于 logging.info()。你得到什么错误信息?
  • 我可以只传递一串额外信息吗?像这样:“员工 ID 1029382 发生错误” 没有创建任何字典并传递密钥
【解决方案2】:

您可以使用LoggerAdapter,这样您就不必在每次记录调用时都传递额外的信息:

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

日志(类似)

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

Filters 也可用于添加上下文信息。

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

产生类似的日志记录。

【讨论】:

  • 我们如何在config.ini 文件中指定它?我想添加当前主机名socket.gethostname()
  • 我的这个样本不适合我。 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)
  • 是否可以添加一个等于“levelname”的字段“level”?见:How can I rename “levelname” to “level” in Python log messages?
  • 我可以只传递一串额外信息吗?像这样的东西:“员工 ID 1029382 发生错误”没有创建任何字典。
【解决方案3】:

另一种方法是创建自定义 LoggerAdapter。当您无法更改格式或您的格式与不发送唯一密钥的代码共享时(在您的情况下为 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

在您的代码中,您将照常创建和初始化您的记录器:

    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)

最后,您将创建包装适配器以根据需要添加前缀:

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

输出将如下所示:

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

【讨论】:

    【解决方案4】:

    使用 mr2ert 的回答,我想出了这个舒适的解决方案(虽然我认为不推荐) - 覆盖内置的日志记录方法以接受自定义参数并在方法中创建 extra 字典:

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

    输出:

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

    这是内置函数供参考:

    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)
    

    【讨论】:

      【解决方案5】:

      Python3

      从 Python3.2 开始,您现在可以使用 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
      

      当然,record_factory 可以自定义为任何可调用对象,如果您保留对工厂可调用对象的引用,custom_attribute 的值可以更新。

      为什么这比使用适配器/过滤器更好?

      • 您无需在应用程序中传递您的记录器
      • 它实际上适用于使用自己的记录器(只需调用logger = logging.getLogger(..))的第 3 方库现在将具有相同的日志格式。 (过滤器/适配器不是这种情况,您需要使用相同的记录器对象)
      • 您可以堆叠/链接多个工厂

      【讨论】:

      • python 2.7 有什么替代品吗?
      • 没有相同的好处,2.7 你必须使用适配器或过滤器。
      • 这是当今python3的最佳答案
      • 根据docs.python.org/3/howto/logging-cookbook.html:这种模式允许不同的库将工厂链接在一起,只要它们不覆盖彼此的属性或无意覆盖作为标准提供的属性,应该没有意外.但是,应该记住,链中的每个链接都会为所有日志记录操作增加运行时开销,并且只有在使用过滤器不能提供所需结果时才应使用该技术。
      • @steve0hh 期望的关键结果之一是能够跨不同的库/模块记录上下文信息,这只能通过这种方式实现。在大多数情况下,库不应该接触记录器配置,这是父应用程序的责任。
      【解决方案6】:

      导入日志;

      类 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: %(message)s');

      类记录器:

      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)
      

      类测试: logger = Logger.getLogger('Test')

      if __name__=='__main__':
          logger.logMessage('ERROR','123','This is an error')
      

      【讨论】:

      • 这个实现会非常低效。
      【解决方案7】:

      我在自己实施后发现了这个 SO 问题。希望它可以帮助某人。在下面的代码中,我在记录器格式中引入了一个名为 claim_id 的额外键。只要环境中存在claim_id 密钥,它就会记录claim_id。在我的用例中,我需要为 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")
      

      要点:https://gist.github.com/ramanujam/306f2e4e1506f302504fb67abef50652

      【讨论】:

        【解决方案8】:

        接受的答案没有将格式记录在日志文件中,而格式反映在 sys 输出中。 或者,我使用了一种更简单的方法并作为;

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

        【讨论】:

          【解决方案9】:

          如果你需要一个默认的extra 映射,并且你想为临时日志消息自定义它,这可以在 Python 2.7+ 中通过创建一个合并默认 @ 的 LoggerAdapter 987654323@ 字典,带有来自给定消息的任何 extra

          import logging
          import os
          import sys
          
          logging.basicConfig(
              level=logging.DEBUG,
              format='%(asctime)s %(levelname)-8s Py%(python)-4s pid:%(pid)-5s %(message)s',
          )
          _logger = logging.getLogger("my-logger")
          _logger.setLevel(logging.DEBUG)
          
          
          class DefaultExtrasAdapter(logging.LoggerAdapter):
              def __init__(self, logger, extra):
                  super(DefaultExtrasAdapter, self).__init__(logger, extra)
          
              def process(self, msg, kwargs):
                  # Speed gain if no extras are present
                  if "extra" in kwargs:
                      copy = dict(self.extra).copy()
                      copy.update(kwargs["extra"])
                      kwargs["extra"] = copy
                  else:
                      kwargs["extra"] = self.extra
                  return msg, kwargs
          
          
          LOG = DefaultExtrasAdapter(_logger, {"python": sys.version_info[0], "pid": os.getpid()})
          
          if __name__ == "__main__":
              LOG.info("<-- With defaults")
              LOG.info("<-- With my version", extra={"python": 3.10})
              LOG.info("<-- With my pid", extra={"pid": 0})
              LOG.info("<-- With both", extra={"python": 2.7, "pid": -1})
          

          结果:

          2021-08-05 18:58:27,308 INFO     Py2    pid:8435  <-- With defaults
          2021-08-05 18:58:27,309 INFO     Py3.1  pid:8435  <-- With my version
          2021-08-05 18:58:27,309 INFO     Py2    pid:0     <-- With my pid
          2021-08-05 18:58:27,309 INFO     Py2.7  pid:-1    <-- With both
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-01-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-12-12
            • 2019-02-16
            • 1970-01-01
            相关资源
            最近更新 更多