日志基础与Python日志模块:从入门到实战
日志这东西,说白了就是程序的「黑匣子」。
我刚入行那会儿,觉得日志可有可无。代码能跑就行,写什么日志?直到有一次线上服务半夜挂了,我盯着空荡荡的控制台,连错误原因都找不到。嗯,从那以后,我再也不敢轻视日志了。
为什么需要日志?
你想想看,一个生产环境的应用,你不可能一直盯着屏幕。日志就是你的眼睛。它能告诉你:
- 程序什么时候启动、什么时候关闭
- 用户做了什么操作
- 哪里出了异常
- 性能瓶颈在哪
我个人习惯,每个关键业务节点都会埋日志。不是瞎埋,是有策略地埋。
Python logging 模块初探
Python 自带的 logging 模块,功能其实很强。很多人觉得它复杂,其实是因为没搞懂它的设计思路。
核心思想:日志系统 = 记录器(Logger)+ 处理器(Handler)+ 格式化器(Formatter)
这三者的关系,我画了张图帮你理解:
日志级别:不是所有消息都值得记录
logging 模块定义了 5 个级别,从低到高:
| 级别 | 数值 | 什么时候用 |
|---|---|---|
| DEBUG | 10 | 调试信息,开发时用。生产环境建议关掉 |
| INFO | 20 | 正常流程信息,比如「用户登录成功」 |
| WARNING | 30 | 有问题但不影响运行,比如「磁盘空间不足80%」 |
| ERROR | 40 | 出错了,但程序还能继续跑 |
| CRITICAL | 50 | 严重错误,程序可能撑不住了 |
我的经验:生产环境一般设为 INFO 级别。DEBUG 日志太多,会拖慢性能。我曾经在一个高并发项目里开了 DEBUG,结果日志文件每小时涨 2GB…… 服务器直接报警了。
Handler:日志去哪儿,你说了算
Handler 决定了日志的「目的地」。常用的有:
StreamHandler— 输出到控制台FileHandler— 输出到文件RotatingFileHandler— 按文件大小滚动TimedRotatingFileHandler— 按时间滚动
我一般这么配:开发环境用控制台,生产环境用文件 + 按天滚动。
import logging
from logging.handlers import RotatingFileHandler
# 创建一个记录器
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 文件处理器(按大小滚动)
file_handler = RotatingFileHandler(
'app.log',
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
file_handler.setLevel(logging.DEBUG)
# 添加处理器
logger.addHandler(console_handler)
logger.addHandler(file_handler)
注意:如果不设置 setLevel,默认会继承 Logger 的级别。我曾经踩过这个坑——明明 Logger 设了 INFO,但 Handler 没设,结果 DEBUG 日志全打出来了。
Formatter:让日志看起来舒服点
Formatter 就是给日志「化妆」的。默认的格式太丑了,我一般会自定义:
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_handler.setFormatter(formatter)
常用的格式化字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| %(asctime)s | 时间 | 2024-01-15 14:30:22 |
| %(name)s | 记录器名称 | my_app |
| %(levelname)s | 日志级别 | INFO |
| %(message)s | 日志内容 | 用户登录成功 |
| %(filename)s | 文件名 | app.py |
| %(lineno)d | 行号 | 42 |
配置文件化:别再硬编码了
把日志配置写在代码里,改起来太麻烦。我推荐用配置文件。
先写一个 logging.conf:
[loggers]
keys=root,my_app
[handlers]
keys=console_handler,file_handler
[formatters]
keys=simple_formatter,detailed_formatter
[logger_root]
level=WARNING
handlers=console_handler
[logger_my_app]
level=DEBUG
handlers=console_handler,file_handler
qualname=my_app
propagate=0
[handler_console_handler]
class=StreamHandler
level=INFO
formatter=simple_formatter
args=(sys.stdout,)
[handler_file_handler]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=detailed_formatter
args=('app.log', 'a', 10*1024*1024, 5)
[formatter_simple_formatter]
format=%(levelname)s - %(message)s
[formatter_detailed_formatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
然后在代码里加载:
import logging.config
logging.config.fileConfig('logging.conf')
logger = logging.getLogger('my_app')
logger.info('配置加载成功!')
小技巧:配置文件支持变量替换。比如 %(log_dir)s,可以在代码里传参。我经常用这个来区分不同环境的日志目录。
避坑指南
这些年我踩过的坑,分享给你:
- 重复日志:每次调用
getLogger都会创建新实例?不会。但如果你多次addHandler,日志会重复输出。解决办法:先清空再添加。 - 日志丢失:程序崩溃时,日志可能没来得及写入。记得设置
flush或者用logging.handlers.MemoryHandler做缓冲。 - 性能问题:日志里拼接字符串很费性能。用
logger.debug('用户 %s 登录', username)而不是f'用户 {username} 登录'。前者只在需要时才格式化。
好了,日志基础就讲这么多。记住一句话:日志不是写给别人看的,是写给未来的自己看的。