【问题标题】:Logging variable data with new format string使用新格式字符串记录变量数据
【发布时间】:2012-10-19 08:15:48
【问题描述】:

我使用 python 2.7.3 的日志记录工具。 Documentation for this Python version say:

日志记录包早于更新的格式化选项,例如 str.format() 和 string.Template。支持这些较新的格式选项...

我喜欢带有花括号的“新”格式。所以我正在尝试做类似的事情:

 log = logging.getLogger("some.logger")
 log.debug("format this message {0}", 1)

并得到错误:

TypeError:字符串格式化期间并非所有参数都转换

我在这里想念什么?

附:我不想使用

log.debug("format this message {0}".format(1))

因为在这种情况下,无论记录器级别如何,消息总是被格式化。

【问题讨论】:

  • 你可以这样做:log.debug("format this message%d" % 1)
  • 你需要配置Formatter使用'{'作为样式
  • @ronak 感谢您的建议,但没有。请参阅“附注”为什么。 BTW log.debug("format this message%d", 1) - 工作正常。
  • @mata 怎么配置?是否有这样做的直接文件?
  • @mata 我找到了。请将其设为答案,以便我将其设置为“正确答案。再次感谢您。

标签: python python-2.7 string-formatting


【解决方案1】:

更新:现在 PyPI 上有一个名为 bracelogger 的包,它实现了下面详述的解决方案。


要为日志消息启用大括号样式格式,我们可以对一些记录器代码进行猴子补丁。

以下修补 logging 模块以创建一个 get_logger 函数,该函数将返回一个记录器,该记录器对其处理的每个日志记录使用新样式格式。

import functools
import logging
import types

def _get_message(record):
    """Replacement for logging.LogRecord.getMessage
    that uses the new-style string formatting for
    its messages"""
    msg = str(record.msg)
    args = record.args
    if args:
        if not isinstance(args, tuple):
            args = (args,)
        msg = msg.format(*args)
    return msg

def _handle_wrap(fcn):
    """Wrap the handle function to replace the passed in
    record's getMessage function before calling handle"""
    @functools.wraps(fcn)
    def handle(record):
        record.getMessage = types.MethodType(_get_message, record)
        return fcn(record)
    return handle

def get_logger(name=None):
    """Get a logger instance that uses new-style string formatting"""
    log = logging.getLogger(name)
    if not hasattr(log, "_newstyle"):
        log.handle = _handle_wrap(log.handle)
    log._newstyle = True
    return log

用法:

>>> log = get_logger()
>>> log.warning("{!r}", log)
<logging.RootLogger object at 0x4985a4d3987b>

注意事项:

  • 与普通的日志记录方法完全兼容(只需将logging.getLogger 替换为get_logger
  • 只会影响由get_logger 函数创建的特定记录器(不会破坏第3 方包)。
  • 消息的格式会延迟到输出(如果日志消息被过滤,则根本不会)。
  • Args 像往常一样存储在 logging.LogRecord 对象上(在某些情况下对自定义日志处理程序很有用)。
  • 适用于从 2.7 到 3.10 的所有 Python 版本。

【讨论】:

  • 唯一的答案认为只有在要打印调试器消息时才应该计算调试字符串。谢谢!
【解决方案2】:

结合string.Formatter添加pprint.pformat类型转换和来自logging:setLogRecordFactory,setLoggerClass。有一个巧妙的技巧——我为Logger._log 方法的参数args 创建额外的嵌套元组,然后在LogRecord init 中解压缩它以省略Logger.makeRecord 中的覆盖。将log.f wraps 每个属性(有意记录方法)与use_format 一起使用,因此您不必显式编写它。此解决方案向后兼容。

from collections import namedtuple
from collections.abc import Mapping                                                     
from functools import partial                                                          
from pprint import pformat                                                              
from string import Formatter                                                        
import logging                
                                                       
                                                                                        
Logger = logging.getLoggerClass()                                                       
LogRecord = logging.getLogRecordFactory()                                               
                                                                                      
                                                                                        
class CustomFormatter(Formatter):                                                       
    def format_field(self, value, format_spec):                                         
        if format_spec.endswith('p'):                                                   
            value = pformat(value)                                                      
            format_spec = format_spec[:-1]                                              
        return super().format_field(value, format_spec)                                 
                                                                                        
                                                                                        
custom_formatter = CustomFormatter()                                                    
                                                                                        
                                                                                        
class LogWithFormat:                                                                    
    def __init__(self, obj):                                                            
        self.obj = obj                                                                  
                                                                                        
    def __getattr__(self, name):                                                        
        return partial(getattr(self.obj, name), use_format=True)    

                
ArgsSmuggler = namedtuple('ArgsSmuggler', ('args', 'smuggled'))                                                                                     
                                                                                        

class CustomLogger(Logger):                                                             
    def __init__(self, *ar, **kw):                                                      
        super().__init__(*ar, **kw)                                                     
        self.f = LogWithFormat(self)                                                    
                                                                                        
    def _log(self, level, msg, args, *ar, use_format=False, **kw):                                         
        super()._log(level, msg, ArgsSmuggler(args, use_format), *ar, **kw)                                     
                                                                                        
                                                                                        
class CustomLogRecord(LogRecord):                                                       
    def __init__(self, *ar, **kw):                                                   
        args = ar[5]
        # RootLogger use CustomLogRecord but not CustomLogger
        # then just unpack only ArgsSmuggler instance
        args, use_format = args if isinstance(args, ArgsSmuggler) else (args, False)                                       
        super().__init__(*ar[:5], args, *ar[6:], **kw)                                  
        self.use_format = use_format                                                    
                                                                                        
    def getMessage(self):                                                               
        return self.getMessageWithFormat() if self.use_format else super().getMessage() 
                                                                                        
    def getMessageWithFormat(self):                                                     
        msg = str(self.msg)                                                             
        args = self.args                                                                
        if args:                                                                        
            fmt = custom_formatter.format                                               
            msg = fmt(msg, **args) if isinstance(args, Mapping) else fmt(msg, *args)    
        return msg                                                                      
                                                                                            
                                                                                                
logging.setLogRecordFactory(CustomLogRecord)      
logging.setLoggerClass(CustomLogger)              
                                                  
log = logging.getLogger(__name__)   
log.info('%s %s', dict(a=1, b=2), 5)          
log.f.info('{:p} {:d}', dict(a=1, b=2), 5)

【讨论】:

    【解决方案3】:

    与 pR0Ps' 类似的解决方案,将 getMessage 包装在 LogRecord 中,方法是在应该启用新格式的 Logger 实例中包装 makeRecord(而不是 handle):

    def getLogger(name):
        log = logging.getLogger(name)
        def Logger_makeRecordWrapper(name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
            self = log
            record = logging.Logger.makeRecord(self, name, level, fn, lno, msg, args, exc_info, func, sinfo)
            def LogRecord_getMessageNewStyleFormatting():
                self = record
                msg = str(self.msg)
                if self.args:
                    msg = msg.format(*self.args)
                return msg
            record.getMessage = LogRecord_getMessageNewStyleFormatting
            return record
        log.makeRecord = Logger_makeRecordWrapper
        return log
    

    我用 Python 3.5.3 对此进行了测试。

    【讨论】:

    • 这决定了实际插入字符串的负载在哪里。您是在创建记录时预先加载它,确保静态字符串是从后端转义的,还是仅在消息最终显示时才执行格式化。简单案例:消息实际上低于可接受的显示水平。另外:这不是“修补”事物的好方法。实际上构造一个 Logger 子类并使用它,伙计。
    【解决方案4】:

    当我发现日志记录仅使用 printf 样式格式时,这是我对问题的解决方案。它允许记录调用保持不变——没有特殊语法,例如log.info(__("val is {}", "x"))。代码所需的更改是将记录器包装在 StyleAdapter 中。

    from inspect import getargspec
    
    class BraceMessage(object):
        def __init__(self, fmt, args, kwargs):
            self.fmt = fmt
            self.args = args
            self.kwargs = kwargs
    
        def __str__(self):
            return str(self.fmt).format(*self.args, **self.kwargs)
    
    class StyleAdapter(logging.LoggerAdapter):
        def __init__(self, logger):
            self.logger = logger
    
        def log(self, level, msg, *args, **kwargs):
            if self.isEnabledFor(level):
                msg, log_kwargs = self.process(msg, kwargs)
                self.logger._log(level, BraceMessage(msg, args, kwargs), (), 
                        **log_kwargs)
    
        def process(self, msg, kwargs):
            return msg, {key: kwargs[key] 
                    for key in getargspec(self.logger._log).args[1:] if key in kwargs}
    

    用法是:

    log = StyleAdapter(logging.getLogger(__name__))
    log.info("a log message using {type} substitution", type="brace")
    

    值得注意的是,如果用于大括号替换的关键字包括levelmsgargsexc_infoextrastack_info,则此实现存在问题。这些是Loggerlog 方法使用的参数名称。如果您需要这些名称之一,请修改 process 以排除这些名称,或者从 _log 调用中删除 log_kwargs。此外,此实现还默默地忽略了用于 Logger 的拼写错误的关键字(例如 ectra)。

    【讨论】:

    【解决方案5】:

    我创建了一个名为ColorFormatter 的自定义格式化程序,它处理这样的问题:

    class ColorFormatter(logging.Formatter):
    
        def format(self, record):
            # previous stuff, copy from logging.py…
    
            try:  # Allow {} style
                message = record.getMessage()  # printf
            except TypeError:
                message = record.msg.format(*record.args)
    
            # later stuff…
    

    这使它与各种库兼容。 缺点是由于可能会尝试两次格式化字符串,因此它可能性能不佳。

    【讨论】:

      【解决方案6】:

      这里有一些非常简单的方法:

      debug_logger: logging.Logger = logging.getLogger("app.debug")
      
      def mydebuglog(msg: str, *args, **kwargs):
          if debug_logger.isEnabledFor(logging.DEBUG):
              debug_logger.debug(msg.format(*args, **kwargs))
      

      然后:

      mydebuglog("hello {} {val}", "Python", val="World")
      

      【讨论】:

        【解决方案7】:

        更简单的解决方案是使用excellent logbook module

        import logbook
        import sys
        
        logbook.StreamHandler(sys.stdout).push_application()
        logbook.debug('Format this message {k}', k=1)
        

        或者更完整的:

        >>> import logbook
        >>> import sys
        >>> logbook.StreamHandler(sys.stdout).push_application()
        >>> log = logbook.Logger('MyLog')
        >>> log.debug('Format this message {k}', k=1)
        [2017-05-06 21:46:52.578329] DEBUG: MyLog: Format this message 1
        

        【讨论】:

        • 这看起来不错,但有没有办法让毫秒而不是几秒?
        • @Jeff 当然,日志允许您使用自定义字符串格式定义自定义处理程序。
        • @Jeff 几年后 - 默认时间精度为毫秒。
        【解决方案8】:

        在 Python 3.2+ 中尝试logging.setLogRecordFactory

        import collections
        import logging
        
        
        class _LogRecord(logging.LogRecord):
        
            def getMessage(self):
                msg = str(self.msg)
                if self.args:
                    if isinstance(self.args, collections.Mapping):
                        msg = msg.format(**self.args)
                    else:
                        msg = msg.format(*self.args)
                return msg
        
        
        logging.setLogRecordFactory(_LogRecord)
        

        【讨论】:

        • 它确实有效,但问题是您破坏了使用 % 格式的第三方模块,因为记录工厂对于日志记录模块是全局的。
        【解决方案9】:

        这是另一个选项,没有沙丘回答中提到的关键字问题。它只能处理位置 ({0}) 参数而不是关键字 ({foo}) 参数。它也不需要两次调用格式化(使用下划线)。它确实具有子类化 str 的 ick 因素:

        class BraceString(str):
            def __mod__(self, other):
                return self.format(*other)
            def __str__(self):
                return self
        
        
        class StyleAdapter(logging.LoggerAdapter):
        
            def __init__(self, logger, extra=None):
                super(StyleAdapter, self).__init__(logger, extra)
        
            def process(self, msg, kwargs):
                if kwargs.pop('style', "%") == "{":  # optional
                    msg = BraceString(msg)
                return msg, kwargs
        

        你可以这样使用它:

        logger = StyleAdapter(logging.getLogger(__name__))
        logger.info("knights:{0}", "ni", style="{")
        logger.info("knights:{}", "shrubbery", style="{")
        

        当然,您可以去掉# optional 的检查,强制所有通过适配器的消息使用新格式。


        多年后阅读此答案的任何人的注意事项:从 Python 3.2 开始,您可以使用 use the style parameterFormatter 对象:

        日志记录(自 3.2 起)改进了对这两种附加格式样式的支持。这 Formatter 类已得到增强,可采用名为 style 的附加可选关键字参数。这 默认为'%',但其他可能的值为'{''$',它们对应于另外两个 格式化样式。默认情况下保持向后兼容性(如您所料),但通过 显式指定样式参数,您可以指定有效的格式字符串 与str.format()string.Template

        文档提供了示例 logging.Formatter('{asctime} {name} {levelname:8s} {message}', style='{')

        请注意,在这种情况下,您仍然无法使用新格式调用 logger。即,以下仍然不起作用:

        logger.info("knights:{say}", say="ni")  # Doesn't work!
        logger.info("knights:{0}", "ni")  # Doesn't work either
        

        【讨论】:

        • 您关于 Python 3 的陈述不正确。 style 参数仅适用于 Formatter 格式字符串,不适用于单个日志消息。您链接到的页面明确表示:“在保持向后兼容性的同时不会改变这一点”。
        • 感谢您让我诚实。第一部分现在用处不大,但我用Formatter 重新表述了它,现在是正确的(我认为)。 StyleAdapter 仍然有效,
        • @falstro -- 感谢您指出这一点。更新的版本现在应该可以工作了。由于BraceString 是一个字符串子类,所以从__str__ 返回自身是安全的
        • 唯一提到 style="{", +1 的答案
        【解决方案10】:

        编辑: 看看StyleAdapter approach in @Dunes' answer 不像这个答案;它允许在调用记录器的方法(debug()、info()、error() 等)时使用其他格式样式而无需样板。


        来自文档 — Use of alternative formatting styles

        记录调用(logger.debug()、logger.info() 等)只需要 实际日志消息本身的位置参数,与 关键字参数仅用于确定如何处理的选项 实际的日志调用(例如 exc_info 关键字参数 表示应该记录回溯信息,或者额外的 关键字参数来指示附加的上下文信息 添加到日志中)。所以你不能直接使用 str.format() 或 string.Template 语法,因为在内部记录 包使用 %-formatting 合并格式字符串和变量 论据。在向后保留的同时不会改变这一点 兼容性,因为现有的所有日志记录调用 代码将使用 %-format 字符串。

        还有:

        不过,有一种方法可以使用 {}- 和 $- 格式化 构建您的个人日志消息。回想一下你的消息 可以使用任意对象作为消息格式字符串,并且 logging 包将在该对象上调用 str() 以获取实际的 格式化字符串。

        复制粘贴到wherever模块:

        class BraceMessage(object):
            def __init__(self, fmt, *args, **kwargs):
                self.fmt = fmt
                self.args = args
                self.kwargs = kwargs
        
            def __str__(self):
                return self.fmt.format(*self.args, **self.kwargs)
        

        然后:

        from wherever import BraceMessage as __
        
        log.debug(__('Message with {0} {name}', 2, name='placeholders'))
        

        注意:实际格式化会延迟到需要时,例如,如果未记录 DEBUG 消息,则根本不执行格式化。

        【讨论】:

        • 从 Python 3.6 开始,您可以像这样使用 f 字符串:num = 2; name = 'placeholders'; log.debug(f'Message with {num} {name}')
        • @P1h3r1e3d13 与答案中的日志记录代码不同,f''-strings 立即执行格式化。
        • 对。它们在这里工作是因为它们在调用 log 方法之前格式化并返回一个常规字符串。这可能与某人相关,也可能不相关,因此我认为值得一提。
        • @Jacktose 我认为用户不应该使用 f-strings 登录,它会破坏日志聚合服务(例如哨兵)。 stdlib 日志记录延迟字符串模板是有充分理由的。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-11
        • 1970-01-01
        • 2020-12-12
        • 2022-11-23
        • 2018-10-02
        相关资源
        最近更新 更多