4. 日志格式化:Formatter详解、自定义日志格式、添加时间/进程/线程信息
日志记录这件事,说白了就是把程序运行时发生了什么给记下来。但记什么、怎么记,这里面的门道可不少。我见过不少新手,日志打了一堆,真出问题的时候翻半天找不到关键信息——这就是格式化没做好。
今天咱们就聊聊 Formatter,这个 Python logging 模块里负责「排版」的家伙。
4.1 Formatter 是什么?
Formatter 的作用很简单:把日志记录(LogRecord)转换成字符串。你可以把它理解成一个模板引擎,它定义了每条日志最终长什么样。
默认情况下,Python 的日志格式是这样的:
WARNING:root:磁盘空间不足
嗯,说实话,这格式在生产环境基本不够用。谁打的?什么时候打的?哪个进程?全都没有。
所以我们需要自定义。
4.2 基本用法
先看一个最简单的例子:
import logging
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger = logging.getLogger('my_app')
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
logger.info('服务启动成功')
输出:
2025-01-15 10:30:45,123 - my_app - INFO - 服务启动成功
你看,加上时间戳和日志器名称后,信息一下子就清晰了。
%(asctime)s 默认格式是 "YYYY-MM-DD HH:MM:SS,mmm"。如果你想要更精确的时间,可以配合 datefmt 参数调整。
4.3 常用格式化字段
Python 提供了很多内置字段,我挑几个最常用的列出来:
| 字段 | 含义 | 示例 |
|---|---|---|
%(asctime)s |
时间戳 | 2025-01-15 10:30:45,123 |
%(name)s |
日志器名称 | my_app |
%(levelname)s |
日志级别 | INFO |
%(message)s |
日志消息 | 服务启动成功 |
%(filename)s |
文件名 | server.py |
%(lineno)d |
行号 | 42 |
%(funcName)s |
函数名 | start_server |
%(process)d |
进程ID | 12345 |
%(thread)d |
线程ID | 140735202312448 |
%(threadName)s |
线程名 | MainThread |
asctime、levelname、name、message 这四个字段。如果涉及多进程/多线程,process 和 threadName 也必不可少。
4.4 自定义时间格式
我个人习惯用 ISO 8601 格式,不带毫秒:
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
datefmt='%Y-%m-%dT%H:%M:%S'
)
输出:
2025-01-15T10:30:45 [INFO] my_app: 服务启动成功
为什么用 ISO 格式?因为很多日志分析工具(比如 ELK、Splunk)默认就能解析它,省去你额外配置的麻烦。
4.5 添加进程和线程信息
我在项目中遇到过一个问题:一个 Web 服务同时处理几百个请求,日志里全是 "用户登录成功",根本分不清是哪个请求。后来加上进程和线程信息,问题迎刃而解。
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] [PID:%(process)d] [TID:%(thread)d] %(name)s: %(message)s'
)
输出:
2025-01-15T10:30:45 [INFO] [PID:12345] [TID:140735202312448] my_app: 用户登录成功
你想想看,如果用的是多进程架构(比如 Gunicorn 开多个 worker),%(process)d 能帮你快速定位是哪个 worker 出了问题。而 %(thread)d 在排查并发问题时更是利器。
%(threadName)s。不过线程名可能重复(比如多个线程池都用 "Thread-1"),所以建议两者结合使用。
4.6 自定义格式化器
有时候内置字段不够用,比如我想在日志里加上主机名。这时候可以继承 logging.Formatter:
import logging
import socket
class CustomFormatter(logging.Formatter):
def __init__(self, fmt=None, datefmt=None):
super().__init__(fmt, datefmt)
self.hostname = socket.gethostname()
def format(self, record):
record.hostname = self.hostname
return super().format(record)
fmt = '%(asctime)s [%(hostname)s] [%(levelname)s] %(message)s'
formatter = CustomFormatter(fmt)
输出:
2025-01-15T10:30:45 [web-server-01] [INFO] 服务启动成功
嗯,这里要注意:自定义字段必须通过 record.xxx = xxx 的方式注入,否则格式化时会报错。
4.7 实战:生产环境日志格式
最后分享一个我目前在用的生产环境格式,供你参考:
formatter = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | '
'[PID:%(process)d] [TID:%(thread)d] | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
输出:
2025-01-15 10:30:45 | INFO | my_app:start_server:42 | [PID:12345] [TID:140735202312448] | 服务启动成功
这个格式有几个好处:
- 用竖线分隔字段,方便用
awk或cut做命令行分析 %(levelname)-8s让级别左对齐,日志看起来整齐- 包含文件名、函数名、行号,出问题能直接定位代码位置
好了,关于 Formatter 的内容就这些。记住一句话:日志格式的设计,决定了你排查问题的效率。花 10 分钟把格式配好,能省下未来 10 小时的排查时间。