日志基础与Python日志模块:从入门到实战

日志这东西,说白了就是程序的「黑匣子」。

我刚入行那会儿,觉得日志可有可无。代码能跑就行,写什么日志?直到有一次线上服务半夜挂了,我盯着空荡荡的控制台,连错误原因都找不到。嗯,从那以后,我再也不敢轻视日志了。

为什么需要日志?

你想想看,一个生产环境的应用,你不可能一直盯着屏幕。日志就是你的眼睛。它能告诉你:

  • 程序什么时候启动、什么时候关闭
  • 用户做了什么操作
  • 哪里出了异常
  • 性能瓶颈在哪

我个人习惯,每个关键业务节点都会埋日志。不是瞎埋,是有策略地埋。

Python logging 模块初探

Python 自带的 logging 模块,功能其实很强。很多人觉得它复杂,其实是因为没搞懂它的设计思路。

核心思想:日志系统 = 记录器(Logger)+ 处理器(Handler)+ 格式化器(Formatter)

这三者的关系,我画了张图帮你理解:

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} 登录'。前者只在需要时才格式化。

好了,日志基础就讲这么多。记住一句话:日志不是写给别人看的,是写给未来的自己看的。

专注资料整理