Python日志记录:在时间格式中使用毫秒

272
默认情况下,logging.Formatter('%(asctime)s') 以以下格式打印:
2011-06-09 10:54:40,638

在编程中,638代表的是毫秒。我需要将逗号改成点:

2011-06-09 10:54:40.638

我可以使用以下代码格式化时间:

logging.Formatter(fmt='%(asctime)s',datestr=date_format_str)

然而文档没有说明如何格式化毫秒。我找到了这个SO问题,它讨论了微秒,但是a)我更喜欢毫秒,b)由于%f,以下内容在Python 2.6上无法工作(我正在使用该版本):

logging.Formatter(fmt='%(asctime)s',datefmt='%Y-%m-%d,%H:%M:%S.%f')

1
也许更改区域设置可以帮助解决问题? - pajton
2
@ pajton - 在下面的链接中,它说“asctime()不使用区域设置信息”- http://docs.python.org/library/time.html#time.asctime - Jonathan Livni
2
%f 在 Python 2.7.9 或 3.5.1 上都无法使用。 - Antony Hatchkins
12
这里的对话很不错。我来这里是因为 logging 声称其默认时间格式遵循 ISO 8601。但实际并非如此。它使用空格而非 "T" 分隔时间,使用逗号而非小数点表示分数秒。他们怎么能搞错呢? - Mr. Lance E Sloan
16个回答

510

这也应该可以运行:

logging.Formatter(
    fmt='%(asctime)s.%(msecs)03d',
    datefmt='%Y-%m-%d,%H:%M:%S'
)

20
谢谢。这些是相关文档链接:https://docs.python.org/2/library/logging.html#logrecord-attributes, https://docs.python.org/3/library/logging.html#logrecord-attributes。是否有办法仍然包括时区(%z)?在Python日志中使用ISO8601格式时间(, -> .)会很好。 - Wes Turner
53
这个解决方案存在缺陷,因为如果你的日期格式中包含%z%Z,你希望它出现在毫秒之后,而不是之前。 - wim
5
@wim 这是一个解决方法,你可以使用UTC时间而非本地时间。方法是这样的:logging.Formatter.converter = time.gmtime,这样你就不需要使用 %z%Z 了。或者,你也可以将 logging.Formatter 对象的 default_msec_format 属性更改为 %s,%03d%z%s,%03d%Z - Mark
3
作为对我之前评论的跟进(无法编辑了...),这是我所做的:

from time import gmtime

# 使用UTC时间,而不是本地日期/时间

logging.Formatter.converter = gmtime

logging.basicConfig(datefmt='%Y-%m-%dT%H:%M:%S', format='%(name)s | %(asctime)s.%(msecs)03dZ | %(message)s', level=log_level)
- Mark
2
@Mark,你不能在default_msec_format中嵌入时区(截至Python 3.7),因为只有时间和毫秒被替换。来自于logging源代码:self.default_msec_format % (t, record.msecs) - sourcenouveau
显示剩余4条评论

95
请注意,除非您需要支持所有ISO 8601格式代码,否则Craig McDaniel的解决方案更可取。

logging.Formatter的formatTime方法如下:

def formatTime(self, record, datefmt=None):
    ct = self.converter(record.created)
    if datefmt:
        s = time.strftime(datefmt, ct)
    else:
        t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
        s = "%s,%03d" % (t, record.msecs)
    return s

注意"%s,%03d"中的逗号。这无法通过指定datefmt来修复,因为ct是一个time.struct_time对象,而这些对象不记录毫秒。
如果我们将ct的定义更改为使其成为datetime对象而不是struct_time,那么(至少在现代版本的Python中),我们可以调用ct.strftime,然后我们可以使用%f格式化微秒:
import logging
import datetime as dt

class MyFormatter(logging.Formatter):
    converter=dt.datetime.fromtimestamp
    def formatTime(self, record, datefmt=None):
        ct = self.converter(record.created)
        if datefmt:
            s = ct.strftime(datefmt)
        else:
            t = ct.strftime("%Y-%m-%d %H:%M:%S")
            s = "%s,%03d" % (t, record.msecs)
        return s

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

console = logging.StreamHandler()
logger.addHandler(console)

formatter = MyFormatter(fmt='%(asctime)s %(message)s',datefmt='%Y-%m-%d,%H:%M:%S.%f')
console.setFormatter(formatter)

logger.debug('Jackdaws love my big sphinx of quartz.')
# 2011-06-09,07:12:36.553554 Jackdaws love my big sphinx of quartz.

或者,要获取毫秒数,请将逗号更改为小数点,并省略datefmt参数:

class MyFormatter(logging.Formatter):
    converter=dt.datetime.fromtimestamp
    def formatTime(self, record, datefmt=None):
        ct = self.converter(record.created)
        if datefmt:
            s = ct.strftime(datefmt)
        else:
            t = ct.strftime("%Y-%m-%d %H:%M:%S")
            s = "%s.%03d" % (t, record.msecs)
        return s

...
formatter = MyFormatter(fmt='%(asctime)s %(message)s')
...
logger.debug('Jackdaws love my big sphinx of quartz.')
# 2011-06-09 08:14:38.343 Jackdaws love my big sphinx of quartz.

2
那么%f实际上会给出微秒,而不是毫秒,对吗? - Jonathan Livni
@Jonathan:哎呀,你说得对,%f会给出微秒。我想要获取毫秒最简单的方法就是将逗号改为小数点(请参见上面的编辑)。 - unutbu
3
我认为这是最好的答案,因为它可以让您立即恢复使用标准格式选项。实际上,我需要微秒级别的精度,而这是唯一可行的选项! - trumpetlicks

68

加上毫秒是更好的选择,谢谢。这是我在Blender中使用Python 3.5.3进行修改的方式。

import logging

logging.basicConfig(level=logging.DEBUG, 
    format='%(asctime)s.%(msecs)03d %(levelname)s:\t%(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)
log = logging.getLogger(__name__)
log.info("Logging Info")
log.debug("Logging Debug")

4
目前为止最简单和最干净的选择。不确定为什么你会得到记录器,当你只需调用 logging.info(msg) 等即可,但格式正是我所需的。其他任何寻找可用属性的人都可以在这里查看: https://docs.python.org/3.6/library/logging.html#logrecord-attributes - naphier
1
嗯,有趣的观点,感谢您的评论,这肯定是一个值得思考的问题。是的,我可能只是把它添加为那里正在发生的事情的教训,并确保它在那里,因为我已经要求了多个内容,所以不需要通过'.'进行多次调用以获取它。如果您再次调用.info或.debug,我可能会像您建议的那样直接保存这些内容,以节省参考查找周期。[let info = logging.info] - Master James
1
@naphier,你获取的是记录器而不是“仅仅”调用logging.info(msg),因为通常你想要利用Python的记录器名称层次结构(例如为某些包设置不同的详细程度级别或依赖于%(levelname)产生有用的值)。另请参见例如python3 -c 'import logging; logging.basicConfig(); logging.warning("foo"); logging.getLogger(__name__).warning("bar")' - maxschlepzig

31
我发现最简单的方法是覆盖 default_msec_format :
formatter = logging.Formatter('%(asctime)s')
formatter.default_msec_format = '%s.%03d'

1
有趣,谢谢。但是这在我的 Python 2.7 中不起作用。它可能只适用于某些值的 Python 3.x。 - nealmcb
2
@nealmcb 根据文档,此功能仅在Python 3.3及以上版本中可用。 - Mark
1
非常简单!不幸的是,它仅在使用默认时间格式(即datefmt=None)时有效。当使用不同的日期格式(例如,当您想要打印带有毫秒但没有数据的时间时)此解决方案不适用。 - wovano
如果没有使用 handler,则 logging.Formatter.default_msec_format='%s.%03d' - Smart Manoj

12
我找到了一种两行代码的方法,可以使Python日志模块输出符合RFC 3339(ISO 1801兼容)格式的时间戳,包括正确格式的毫秒和时区信息,并且不需要使用外部依赖库:
import datetime
import logging

# Output timestamp, as the default format string does not include it
logging.basicConfig(format="%(asctime)s: level=%(levelname)s module=%(module)s msg=%(message)s")

# Produce RFC 3339 timestamps
logging.Formatter.formatTime = (lambda self, record, datefmt=None: datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).astimezone().isoformat())

示例:

>>> logging.getLogger().error("Hello, world!")
2021-06-03T13:20:49.417084+02:00: level=ERROR module=<stdin> msg=Hello, world!

或者,最后一行也可以这样写:

def formatTime_RFC3339(self, record, datefmt=None):
    return (
        datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc)
        .astimezone()
        .isoformat()
    )

logging.Formatter.formatTime = formatTime_RFC3339

该方法还可以在特定格式化程序实例上使用,而不是在类级别上进行覆盖。在这种情况下,您需要从方法签名中删除self


8

这里有许多过时、过于复杂和奇怪的答案。原因在于文档不足,简单的解决方案是只需使用basicConfig()并设置如下:

logging.basicConfig(datefmt='%Y-%m-%d %H:%M:%S', format='{asctime}.{msecs:0<3.0f} {name} {threadName} {levelname}: {message}', style='{')

这里的技巧是你必须设置datefmt参数,因为默认值会混乱,并且不是Python文档中显示的内容。所以最好看一下这里
另一种可能更简洁的替代方法是覆盖default_msec_format变量:
formatter = logging.Formatter('%(asctime)s')
formatter.default_msec_format = '%s.%03d'

但出于未知原因,那并没有起作用

PS. 我正在使用 Python 3.8。


不确定你们两个的解决方案是否有效。 - mike rodent
像我说的一样,第一个我用过并且对我有效,第二个是在文档中找到的,但并没有起作用。所以如果你“不确定”,你实际上想说什么? - not2qubit
很抱歉,它不打印时区,因此它不符合ISO8601标准,但如果您只需要毫秒,则可以使用它。 - Bosco Domingo
@BoscoDomingo 您可以调整datefmt字符串,以符合您的要求/需要,还可以包括ISO-8601时间格式中的TZ。 - not2qubit
1
为什么将毫秒添加到datefmt字符串中不起作用? - stam

5

一个简单的扩展,不需要使用 datetime 模块,并且不像其他解决方案那样受到限制,可以使用简单的字符串替换,如下所示:

import logging
import time

class MyFormatter(logging.Formatter):
    def formatTime(self, record, datefmt=None):
        ct = self.converter(record.created)
        if datefmt:
            if "%F" in datefmt:
                msec = "%03d" % record.msecs
                datefmt = datefmt.replace("%F", msec)
            s = time.strftime(datefmt, ct)
        else:
            t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
            s = "%s,%03d" % (t, record.msecs)
        return s

这样,日期格式可以按照您想要的方式编写,甚至可以考虑到地区差异,只需使用%F表示毫秒即可。例如:
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)

sh = logging.StreamHandler()
log.addHandler(sh)

fm = MyFormatter(fmt='%(asctime)s-%(levelname)s-%(message)s',datefmt='%H:%M:%S.%F')
sh.setFormatter(fm)

log.info("Foo, Bar, Baz")
# 03:26:33.757-INFO-Foo, Bar, Baz

4

实例化一个Formatter之后,我通常会设置formatter.converter = gmtime。因此,在这种情况下,为了使@unutbu的答案起作用,您需要:

class MyFormatter(logging.Formatter):
    def formatTime(self, record, datefmt=None):
        ct = self.converter(record.created)
        if datefmt:
            s = time.strftime(datefmt, ct)
        else:
            t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
            s = "%s.%03d" % (t, record.msecs)
        return s

3
如果您正在使用arrow,或者不介意使用箭头符号,那么您可以将Python的时间格式替换为箭头的时间格式。
import logging

from arrow.arrow import Arrow


class ArrowTimeFormatter(logging.Formatter):

    def formatTime(self, record, datefmt=None):
        arrow_time = Arrow.fromtimestamp(record.created)

        if datefmt:
            arrow_time = arrow_time.format(datefmt)

        return str(arrow_time)


logger = logging.getLogger(__name__)

default_handler = logging.StreamHandler()
default_handler.setFormatter(ArrowTimeFormatter(
    fmt='%(asctime)s',
    datefmt='YYYY-MM-DD HH:mm:ss.SSS'
))

logger.setLevel(logging.DEBUG)
logger.addHandler(default_handler)

现在您可以在datefmt属性中使用所有箭头的时间格式

3

如果您更喜欢使用style='{'fmt="{asctime}.{msecs:0<3.0f}"将0填充到微秒的三个位置以保持一致性。


当在 logger.Formatter() 中使用时,这只是添加了 ,nnn 格式(到 %S 部分)。因此,它需要在 basicConfig(datefmt=..., format=..., style='{') 中使用。 - not2qubit

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