一、为什么不能用 print 代替日志?
很多新手刚学 python 时,调试代码全靠 print():
print("用户登录成功")
print("数据库连接失败")
print("订单处理完成")代码量少时没问题,但项目一复杂,print 的缺陷就暴露了:
| 问题 | logging | |
|---|---|---|
| 无法分级 | 所有信息混在一起 | 五级日志,按需过滤 |
| 无法持久化 | 程序结束就没了 | 自动写入文件保存 |
| 无法控制输出 | 只能打印到控制台 | 控制台/文件/邮件/网络随意切换 |
| 性能差 | 无条件执行字符串拼接 | 级别不够时直接跳过,零开销 |
| 无上下文 | 纯字符串,不知道从哪来的 | 时间/模块/行号/级别全记录 |
| 多线程不安全 | 输出可能交错混乱 | 内置锁机制,线程安全 |
一句话总结:print 是往屏幕上吐字符串,logging 是一条完整的"日志生产线"——从产生日志到最终落地,每一步都可控。
二、logging 模块的四大核心组件
python 的 logging 模块采用分层解耦架构,核心由四大组件构成:
你的代码:logger.info("...")
│
▼
┌───────────────┐ ① logger:日志入口,级别过滤的第一道关卡
│ logger │ getlogger('app.api'),命名空间支持层级继承
└───────┬───────┘
│
▼
┌───────────────┐ ② filter:比级别更细的过滤,也可给 record 注入字段
│ filter │ addfilter()
└───────┬───────┘
│
▼
┌───────────────┐ ③ handler:决定日志"去哪",级别过滤的第二道关卡
│ handler │ stream / file / rotating / smtp / socket / queue
└───────┬───────┘
│
▼
┌───────────────┐ ④ formatter:决定日志"长什么样"
│ formatter │ %(asctime)s %(name)s %(levelname)s %(message)s
└───────┬───────┘
│
▼
控制台 / 文件 / 网络 / 邮件 / 消息队列
1. logger(日志器)—— 入口和控制中心
logger 是你直接打交道的对象,主要职责包括:
- 接收日志调用:提供
debug()、info()、warning()、error()、critical()等方法 - 级别过滤:判断日志是否达到设定阈值,决定是否继续处理
- 层级管理:支持以点号分隔的命名空间(如
app.db.query),子 logger 自动继承父 logger 配置 - 分发日志:将日志消息传递给绑定的 handler 进行输出
import logging # 推荐用 __name__ 作为名称,自动形成层级结构 logger = logging.getlogger(__name__) logger.setlevel(logging.debug) # 设置最低日志级别
2. handler(处理器)—— 决定日志去哪
handler 决定日志的输出目标,一个 logger 可以绑定多个 handler:
| handler | 用途 |
|---|---|
streamhandler | 输出到控制台 |
filehandler | 输出到文件 |
rotatingfilehandler | 按文件大小轮转,防止日志撑爆磁盘 |
timedrotatingfilehandler | 按时间轮转(每天/每小时) |
smtphandler | 发送邮件通知 |
httphandler | 发送到远程服务器 |
queuehandler | 异步队列处理,适合高并发场景 |
nullhandler | 库开发用,避免未配置时输出到 stderr |
3. formatter(格式化器)—— 决定日志长什么样
formatter 定义日志的输出格式,通过占位符配置:
| 占位符 | 含义 |
|---|---|
%(asctime)s | 日志产生时间 |
%(name)s | 日志器名称 |
%(levelname)s | 日志级别名称 |
%(message)s | 日志内容 |
%(filename)s | 产生日志的文件名 |
%(lineno)d | 产生日志的代码行号 |
%(funcname)s | 产生日志的函数名 |
4. filter(过滤器)—— 精细化筛选
filter 用于比级别更细粒度的过滤,可基于名称、自定义条件等进行筛选,通过 addfilter() 挂载到 logger 或 handler 上。
三、五种标准日志级别
logger 定义了五个标准级别,数值越大越严重:
| 级别 | 数值 | 用途 | 开发环境 | 生产环境 |
|---|---|---|---|---|
debug | 10 | 调试细节,记录变量值、执行流程 | 开启 | 关闭 |
info | 20 | 正常流程,确认程序按预期运行 | 开启 | 开启 |
warning | 30 | 潜在问题,但不影响运行 | 开启 | 开启 |
error | 40 | 发生错误,功能受影响 | 开启 | 开启 |
critical | 50 | 严重故障,可能导致程序崩溃 | 开启 | 开启 |
默认级别为 warning,只有 ≥ 设定级别的日志才会被处理。
四、logger 的内部执行流程
当调用 logger.info("msg") 时,内部流程如下:
调用 logger 方法
↓
创建 logrecord 对象
↓
logger 级别过滤(是否 ≥ 设定级别?)
↓
filter 过滤(自定义筛选条件)
↓
分发至所有绑定的 handler
↓
handler 级别过滤 → formatter 格式化 → emit() 输出
关键规则:日志级别需同时 ≥ logger 级别和 handler 级别时才会输出。
五、从入门到进阶:三种配置方式
方式一:基础配置(适合简单脚本)
使用 logging.basicconfig() 一行搞定:
import logging
logging.basicconfig(
level=logging.debug,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%y-%m-%d %h:%m:%s",
filename="app.log", # 输出到文件
filemode="a", # 追加模式
encoding="utf-8" # 避免中文乱码
)
logger = logging.getlogger(__name__)
logger.info("服务启动成功")️ basicconfig() 仅在首次调用时生效,第二次调用无效。
方式二:手动组装(适合正式项目)
显式创建 logger、handler 和 formatter,灵活组合:
import logging
def setup_logger():
# 1. 创建 logger
logger = logging.getlogger(__name__)
logger.setlevel(logging.debug)
logger.propagate = false # 禁止向上传播,避免重复输出
# 2. 创建控制台 handler
console_handler = logging.streamhandler()
console_handler.setlevel(logging.info) # 控制台只输出 info 及以上
# 3. 创建文件 handler
file_handler = logging.filehandler("app.log", encoding="utf-8")
file_handler.setlevel(logging.debug) # 文件记录所有级别
# 4. 创建 formatter
formatter = logging.formatter(
"%(asctime)s | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s",
datefmt="%y-%m-%d %h:%m:%s"
)
# 5. 绑定 formatter 到 handler
console_handler.setformatter(formatter)
file_handler.setformatter(formatter)
# 6. 绑定 handler 到 logger
logger.addhandler(console_handler)
logger.addhandler(file_handler)
return logger
# 使用
logger = setup_logger()
logger.debug("调试信息")
logger.info("正常信息")
logger.error("错误信息", exc_info=true) # 记录完整异常堆栈方式三:字典配置(适合大型项目)
通过 logging.config.dictconfig() 加载外部配置,支持 json/yaml 格式:
import logging.config
config = {
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"standard": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.streamhandler",
"level": "debug",
"formatter": "standard",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.filehandler",
"level": "info",
"formatter": "standard",
"filename": "app.log",
"encoding": "utf8"
}
},
"loggers": {
"my_app": {
"level": "debug",
"handlers": ["console", "file"],
"propagate": false
}
}
}
logging.config.dictconfig(config)
logger = logging.getlogger("my_app")六、常用方法速查
| 方法 | 说明 |
|---|---|
logger.debug(msg) | 记录 debug 级别日志 |
logger.info(msg) | 记录 info 级别日志 |
logger.warning(msg) | 记录 warning 级别日志 |
logger.error(msg) | 记录 error 级别日志 |
logger.critical(msg) | 记录 critical 级别日志 |
logger.exception(msg) | 记录 error 级别 + 完整异常堆栈(等价于 error(msg, exc_info=true)) |
logger.log(level, msg) | 动态指定日志级别 |
记录变量的正确写法(使用 %s 占位符,而非 f-string):
# 推荐:延迟格式化,性能更好
logger.info("用户 %s 登录成功,id: %d", username, user_id)
# 不推荐:f-string 会无条件计算
logger.info(f"用户 {username} 登录成功,id: {user_id}")七、日志轮转:避免日志文件撑爆磁盘
当程序长期运行时,单一日志文件会越来越大。logging.handlers 提供了两种轮转机制:
按文件大小轮转(rotatingfilehandler)
from logging.handlers import rotatingfilehandler
handler = rotatingfilehandler(
"app.log",
maxbytes=10 * 1024 * 1024, # 单个文件最大 10mb
backupcount=5, # 保留最近 5 个历史文件
encoding="utf-8"
)按时间轮转(timedrotatingfilehandler)
from logging.handlers import timedrotatingfilehandler
handler = timedrotatingfilehandler(
"app.log",
when="d", # 轮转单位:s(秒)、m(分)、h(时)、d(天)、midnight(午夜)
interval=1, # 间隔时间
backupcount=7, # 保留最近 7 天日志
encoding="utf-8"
)八、新手常见坑点
| 坑点 | 原因 | 解决方案 |
|---|---|---|
| debug 日志不输出 | 默认级别为 warning | 手动调用 logger.setlevel(logging.debug) |
| 日志重复打印 | 多次 addhandler 或 propagate 未关闭 | 调用 logger.handlers.clear() 或设置 propagate=false |
| 日志文件乱码 | 未指定编码 | filehandler 中指定 encoding="utf-8" |
| 设置级别无效 | 只给 logger 设了级别,没给 handler 设 | logger 和 handler 都要设置级别 |
| f-string 性能差 | 即使级别不够也会执行字符串拼接 | 改用 %s 占位符 |
九、最佳实践总结
用 __name__ 命名 logger:便于追踪日志来源,自动形成层级结构
避免重复配置:不要在每个模块调用 basicconfig(),应在应用入口集中配置
库开发加 nullhandler:避免未配置时日志输出到 stderr
生产环境慎用 debug:高频路径输出 debug 日志会影响性能
使用 exc_info=true 或 exception():记录完整异常堆栈,是排障的关键信息
用 %s 占位符而非 f-string:避免不必要的性能开销
设置 propagate=false:避免日志被传递到 root logger,导致重复输出
十、完整生产级示例
import logging
import logging.handlers
import json
from pathlib import path
class jsonformatter(logging.formatter):
"""json 格式化器,便于 elk/loki 采集分析"""
def format(self, record):
log_data = {
"time": self.formattime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getmessage(),
"module": record.module,
"line": record.lineno,
}
if record.exc_info:
log_data["exception"] = self.formatexception(record.exc_info)
return json.dumps(log_data, ensure_ascii=false)
def setup_production_logging(log_dir="logs"):
"""配置生产级日志体系"""
log_path = path(log_dir)
log_path.mkdir(exist_ok=true)
root_logger = logging.getlogger()
root_logger.setlevel(logging.debug)
# 统一格式
fmt = logging.formatter(
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%y-%m-%d %h:%m:%s"
)
# 控制台 handler:只输出 warning 及以上
console = logging.streamhandler()
console.setlevel(logging.warning)
console.setformatter(fmt)
# 文件 handler:输出所有级别,按大小轮转
file_handler = logging.handlers.rotatingfilehandler(
log_path / "app.log",
maxbytes=10 * 1024 * 1024, # 10mb 轮转
backupcount=5,
encoding="utf-8",
)
file_handler.setlevel(logging.debug)
file_handler.setformatter(fmt)
# 错误专用 handler
error_handler = logging.handlers.rotatingfilehandler(
log_path / "error.log",
maxbytes=5 * 1024 * 1024,
backupcount=3,
encoding="utf-8",
)
error_handler.setlevel(logging.error)
error_handler.setformatter(fmt)
root_logger.addhandler(console)
root_logger.addhandler(file_handler)
root_logger.addhandler(error_handler)
# 使用
setup_production_logging()
logger = logging.getlogger(__name__)
logger.info("应用启动")
logger.error("请求失败", exc_info=true)核心思想:logger 负责"记录什么",handler 负责"输出到哪",formatter 负责"长什么样"。这种解耦设计让日志系统既灵活又可扩展。掌握这套体系,你就从"用 print 调试"的新手,迈向了"用 logging 构建可观测系统"的工程师。
以上就是python日志模块logging的新手入门教程的详细内容,更多关于python logging模块的资料请关注代码网其它相关文章!
发表评论