Python日志模块:从入门到实战
日志这东西,说实话,刚入行时我根本没当回事。那时候觉得,代码能跑就行,要什么日志?直到有一次线上出了个诡异的bug,我盯着控制台看了三个小时,愣是没找到问题在哪。后来前辈过来,看了一眼日志文件,两分钟定位了问题。嗯,从那以后,我再也不敢轻视日志了。
今天我们就来聊聊Python的logging模块。这是Python标准库自带的日志工具,功能强大,而且不用装任何第三方包。
为什么选logging,而不是print?
很多新手喜欢用print来调试。我个人习惯是,开发阶段用print没问题,但上了生产环境,print就完全不够用了。你想想看:
- print输出到控制台,没法持久化保存
- print没有级别区分,紧急信息和普通信息混在一起
- print不能控制输出格式,想加个时间戳都得自己拼字符串
- print在多线程环境下会乱序,你根本不知道哪条信息是谁打的
logging模块就是来解决这些问题的。它提供了日志级别、格式化输出、多种输出目标(文件、控制台、网络等),而且线程安全。
快速上手:最简单的日志
先来个最简单的例子,感受一下:
import logging
logging.debug('这是debug信息')
logging.info('这是info信息')
logging.warning('这是warning信息')
logging.error('这是error信息')
logging.critical('这是critical信息')
运行一下,你会发现只有warning、error、critical三条输出了。为什么?因为默认的日志级别是WARNING,低于这个级别的信息会被忽略。
日志级别从低到高: DEBUG → INFO → WARNING → ERROR → CRITICAL
设置级别后,只会输出该级别及以上的日志。
Handler:日志该去哪?
logging模块的核心设计,说白了就是「日志产生」和「日志输出」分离。Logger负责产生日志,Handler负责把日志送到该去的地方。
我在项目中遇到过这样一个场景:开发环境希望日志同时输出到控制台和文件,但生产环境只输出到文件,而且还要按天切割。用Handler就能轻松实现。
常用的Handler有这些:
| Handler类型 | 用途 | 适用场景 |
|---|---|---|
| StreamHandler | 输出到控制台 | 开发调试 |
| FileHandler | 输出到文件 | 简单日志记录 |
| RotatingFileHandler | 按文件大小切割 | 避免单个日志文件过大 |
| TimedRotatingFileHandler | 按时间切割 | 按天/小时归档日志 |
| SMTPHandler | 发送邮件 | 错误告警 |
来看个实际例子:
import logging
# 创建logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建控制台handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
# 创建文件handler
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.INFO)
# 给logger添加handler
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.debug('这条只会在控制台显示')
logger.info('这条控制台和文件都会记录')
小技巧: 我习惯给每个模块创建一个独立的logger,用模块名命名。这样日志里能清楚看到是哪段代码输出的,排查问题特别方便。
Formatter:日志长什么样?
Formatter就是决定日志字符串格式的。默认的格式太简单了,只有日志级别和消息内容。在实际项目中,我们通常需要时间戳、模块名、行号等信息。
常用的格式化字段:
| 字段 | 含义 | 示例输出 |
|---|---|---|
| %(asctime)s | 时间戳 | 2024-01-15 14:30:25,123 |
| %(levelname)s | 日志级别 | INFO |
| %(name)s | logger名称 | my_app |
| %(filename)s | 文件名 | main.py |
| %(lineno)d | 行号 | 42 |
| %(message)s | 日志内容 | 用户登录成功 |
来个完整的例子:
import logging
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 给handler设置formatter
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info('服务启动成功')
# 输出:2024-01-15 14:30:25 - my_app - INFO - main.py:18 - 服务启动成功
注意: 我曾经踩过一个坑——在Formatter里用了中文冒号,结果日志文件编码不对,全变成了乱码。建议所有格式化符号都用英文,时间格式里的分隔符也统一用英文。
配置文件管理:告别硬编码
把日志配置写在代码里,说实话,挺low的。每次改个日志级别都要改代码重新部署,太麻烦了。我建议用配置文件来管理日志配置。
Python支持三种配置方式:
- dictConfig: 用字典配置,最灵活
- fileConfig: 用ini文件配置,简单直观
- 代码配置: 直接写代码,适合简单场景
我个人最常用的是dictConfig,因为它支持复杂配置,而且可以动态修改。来看个例子:
import logging.config
LOGGING_CONFIG = {
'version': 1,
'formatters': {
'standard': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
'detailed': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'standard'
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO',
'formatter': 'detailed',
'filename': 'app.log',
'maxBytes': 10485760, # 10MB
'backupCount': 5
}
},
'loggers': {
'my_app': {
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': False
}
}
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger('my_app')
logger.info('配置加载完成')
最佳实践: 我一般把日志配置放在单独的config.py或yaml文件里。开发环境用DEBUG级别+控制台输出,生产环境用INFO级别+文件输出+按天切割。这样切换环境只需要改配置文件,代码完全不用动。
避坑指南
最后分享几个我踩过的坑:
- 重复日志: 每次调用getLogger时如果重复添加handler,日志会重复输出。解决办法是判断handler是否已存在,或者用dictConfig统一管理。
- 日志泄露: 不要在日志里打印密码、token等敏感信息。我曾经见过有人在error日志里打印了完整的SQL语句,里面包含用户密码...嗯,后果很严重。
- 性能问题: 日志级别设置太低,DEBUG日志太多,会拖慢系统。生产环境建议至少设为INFO。
- 编码问题: 中文日志在Windows下容易乱码,建议统一用UTF-8编码。
好了,logging模块的基础就这些。掌握了这些,你已经能应对大部分日常开发场景了。下一章我们会聊聊更高级的话题——如何把日志和链路追踪结合起来,实现真正的可观测性。