使用新格式字符串记录变量数据

96

我在Python 2.7.3中使用日志记录功能。这个Python版本的文档说:

logging包的发布时间早于str.format()和string.Template等较新的格式选项。但这些较新的格式选项也得到了支持...

我喜欢用花括号的'新'格式。所以我尝试做一些类似以下的事情:

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

并且遇到了错误:

类型错误:在字符串格式化期间并非所有参数都转换

我错过了什么?

附言:我不想使用

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

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


1
你可以这样做: log.debug("格式化此消息%d" % 1) - ronak
1
你需要配置Formatter,以使用'{'作为样式。 - mata
3
谢谢你的建议,但我不需要。请看“附言”部分的原因。顺便说一下,log.debug("format this message%d", 1) - 运行良好。 - MajesticRa
@mata 怎么配置?有直接的文档吗? - MajesticRa
请问标准库什么时候会有解决方案?Q_Q - ThorSummoner
显示剩余2条评论
11个回答

39

编辑:看看@Dunes答案中的StyleAdapter方法,与此答案不同;它允许在调用记录器的方法(debug()、info()、error()等)时使用替代格式样式,而无需使用样板代码。


从文档中了解到 — 使用备选格式样式:

记录调用(logger.debug(),logger.info() 等)仅接受实际记录消息的位置参数,并且关键字参数仅用于确定如何处理实际记录调用的选项(例如 exc_info 关键字参数表示应记录回溯信息,extra 关键字参数表示要添加到日志中的其他上下文信息)。因此,您无法直接使用 str.format() 或 string.Template 语法进行记录调用,因为内部 logging 包使用 %-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消息,则根本不执行格式化。

5
从Python 3.6开始,您可以使用f-字符串,例如: num = 2; name = 'placeholders'; log.debug(f'Message with {num} {name}') - Jacktose
17
与回答中的日志记录代码不同,f''- 字符串会立即执行格式化。 - jfs
1
没错。它们之所以能在这里工作,是因为在调用日志方法之前格式化并返回了一个常规字符串。这可能与某些人有关,所以我认为值得提及作为一种选择。 - Jacktose
7
@Jacktose 我认为用户不应该使用 f-strings 进行日志记录,这会影响日志聚合服务(例如 sentry)。标准库的日志记录之所以推迟字符串模板化,是有很好的理由的。 - wim

30
这里有另一种选项,它没有 Dunes 回答中提到的关键字问题。它只能处理位置参数({0}),而不能处理关键字参数({foo})。它也不需要使用下划线进行两次格式化调用。它确实有子类化 str 的不适感:
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开始,您可以在Formatter对象中使用样式参数(use the style parameter)

日志记录(自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

7
关于Python 3的声明不正确。style参数仅适用于Formatter格式字符串,而不是单个日志消息。你链接的页面明确表示:“在保持向后兼容性的情况下不会更改这一点”。 - mhsmith
1
谢谢你让我保持诚实。第一部分现在不太有用,但我已经重新用“Formatter”来表述了,现在是正确的(我想)。 “StyleAdapter”仍然有效。 - Felipe
@falstro -- 感谢您指出这一点。更新版本现在应该可以工作了。由于BraceString是一个字符串子类,从__str__返回它本身是安全的。 - Felipe
1
只有提到style="{的答案,+1。 - Tom

28
这是我在发现日志记录只使用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_infoextra或者stack_info,则该实现存在问题。这些都是Logger方法中使用的参数名称。如果您需要使用其中一个名称,则修改process以排除这些名称,或者只需从_log调用中删除log_kwargs即可。此外,这个实现还会悄悄地忽略那些拼错了的关键字(例如:ectra)。


4
这种方法是Python文档推荐的,https://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles - eshizhan
我不想批量编辑/更新你的答案,但我已经为Python +3.6 /2023进行了更新。https://dev59.com/Jmcs5IYBdhLWcg3wGgNc#76582731 - David

24

更简单的解决方案是使用优秀的logbook模块

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 当然,Logbook允许您定义自定义处理程序并使用自定义字符串格式。 - Thomas Orozco
5
几年后-默认时间精度为毫秒。 - Jan Vlcinsky

12
现在在PyPI上有一个叫做bracelogger的包,它实现了所需的功能。
项目的README中提供了一个演示:
# import the library
from bracelogger import get_logger

# set up the logger
__log__ = get_logger(__name__)

# use brace-style formatting in log messages
try:
    process(some_obj)
except Exception:
    __log__.warning(
        "Failed to process object '{0!r}' with name '{0.name}' and path '{0.path}'",
        some_obj,
        exc_info=True
    )

注意:

  • 支持广泛的Python版本(v2.7-v3.11)
  • 没有依赖项
  • 没有特殊语法(只需更改logging.getLogger的调用和消息模板即可)
  • 仅为库创建的记录器启用大括号样式格式化。这允许逐渐过渡到大括号样式格式化而不会破坏现有的记录器或第三方包。
  • 在输出日志消息之前,延迟消息的格式化处理(如果日志消息被过滤,则根本不进行格式化处理)。
  • 传入日志调用的args与通常存储在logging.LogRecord对象上。

2
唯一的答案是考虑只有在要打印调试器消息时才计算调试字符串。谢谢! - Fafaman

2
这是对 @Dunes 答案的更新,因为我相信 getargspec 已经在 Python 3.6 中被弃用。此外,这还使用了新的 stacklevel 参数来修复堆栈跟踪。
#log_helper.py
from inspect import getfullargspec
import logging
import typing as T


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:logging.Logger):
        self.logger = logger

    def log(
        self,
        level: int,
        msg: object,
        *args, **kwargs,
    ) -> None:
        if self.isEnabledFor(level):
            msg, log_kwargs = self.process(msg, kwargs)
            # Note the `stacklevel` keyword argument so that funcName and lineno are rendered correctly.
            self.logger._log(level, BraceMessage(msg, args, kwargs), (), stacklevel=2, **log_kwargs)

    def process(self, msg: T.Any, kwargs: T.MutableMapping[str, T.Any]) -> tuple[T.Any, T.MutableMapping[str, T.Any]]:
        # .args[1:] skips over the `self` argument in `logger._log`
        mapped = {key: kwargs[key] for key in getfullargspec(self.logger._log).args[1:] if key in kwargs}
        return msg, mapped

    def addHandler(self, handler):
        self.logger.addHandler(handler)


def getLogger(namespace):
    return StyleAdapter(logging.getLogger(namespace))


你会这样使用它
from log_helper import getLogger

log = getLogger(__name__)

log.info("Hello {}", "world")

没有`stacklevel`参数,"funcName"和"lineno"的值将始终为`StyleAdapter`的`log`方法。

2
尝试在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)

它确实可以工作,但问题在于您会破坏使用“%”格式的第三方模块,因为记录工厂是全局的日志模块。 - jtaylor

1
我创建了一个名为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…

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

0

结合了 string.Formatterpprint.pformat 类型转换以及来自 loggingsetLogRecordFactorysetLoggerClass,有一个巧妙的技巧- 为参数 args 创建额外的嵌套元组,用于 Logger._log 方法,然后在 LogRecord 初始化中解包它,以避免在 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)

0

类似于pR0Ps的解决方案,通过将{{link1:getMessage}}包装在LogRecord中,通过将{{link2:makeRecord}}(而不是他们答案中的handle)包装在应该启用新格式的Logger实例中:

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子类并使用它,伙计。 - amcgregor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接