1. 为什么需要专业的日志记录
在Python开发中,print()是最简单的调试工具,但它在生产环境中存在诸多局限性。我曾经接手过一个线上服务,开发者用print输出调试信息,结果导致:
- 日志文件膨胀到几十GB
- 关键错误信息被淹没在大量调试输出中
- 无法区分不同严重级别的事件
- 缺少时间戳和代码位置信息
专业的日志系统logging模块解决了这些问题,它提供了:
- 分级日志(DEBUG/INFO/WARNING等)
- 多输出目标(文件/控制台/网络等)
- 格式化输出(时间戳/模块名等)
- 日志轮转和归档
2. 基础日志配置实战
2.1 最小化配置示例
python复制import logging
# 基础配置(输出到控制台)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def divide(a, b):
try:
result = a / b
logger.info(f"Division result: {result}")
return result
except ZeroDivisionError:
logger.error("Attempted division by zero!")
return None
divide(10, 2) # 正常输出
divide(10, 0) # 错误输出
输出示例:
code复制INFO:__main__:Division result: 5.0
ERROR:__main__:Attempted division by zero!
2.2 日志级别详解
Python定义了6个标准日志级别:
| 级别 | 数值 | 使用场景 |
|---|---|---|
| CRITICAL | 50 | 系统不可用 |
| ERROR | 40 | 功能故障 |
| WARNING | 30 | 潜在问题 |
| INFO | 20 | 运行状态 |
| DEBUG | 10 | 调试信息 |
| NOTSET | 0 | 继承父级 |
关键技巧:生产环境建议设为WARNING,开发环境设为DEBUG
3. 高级日志配置方案
3.1 文件日志与轮转配置
python复制from logging.handlers import RotatingFileHandler
logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)
# 创建轮转日志处理器
handler = RotatingFileHandler(
"app.log",
maxBytes=5*1024*1024, # 5MB
backupCount=3,
encoding="utf-8"
)
# 自定义格式
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
3.2 多处理器配置示例
python复制def setup_logging():
logger = logging.getLogger("multi_handler")
logger.setLevel(logging.DEBUG)
# 控制台处理器
console = logging.StreamHandler()
console.setLevel(logging.WARNING)
# 文件处理器
file_handler = logging.FileHandler("debug.log")
file_handler.setLevel(logging.DEBUG)
# 邮件处理器(错误报警)
smtp_handler = logging.handlers.SMTPHandler(
mailhost=("smtp.example.com", 587),
fromaddr="alerts@example.com",
toaddrs=["admin@example.com"],
subject="Application Error",
credentials=("user", "password")
)
smtp_handler.setLevel(logging.ERROR)
# 统一格式
formatter = logging.Formatter("%(levelname)s - %(message)s")
for handler in [console, file_handler, smtp_handler]:
handler.setFormatter(formatter)
logger.addHandler(handler)
4. 结构化日志实践
4.1 使用字典传递日志数据
python复制import json
from logging import Formatter
class JsonFormatter(Formatter):
def format(self, record):
log_data = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"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)
# 配置JSON日志
logger = logging.getLogger("json_logger")
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.info("User login", extra={"user": "alice", "ip": "192.168.1.1"})
输出示例:
json复制{
"timestamp": "2023-07-20T14:30:00",
"level": "INFO",
"message": "User login",
"module": "auth",
"line": 42,
"user": "alice",
"ip": "192.168.1.1"
}
4.2 上下文日志记录
python复制import contextvars
request_id = contextvars.ContextVar("request_id")
class ContextFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get("N/A")
return True
logger.addFilter(ContextFilter())
def process_request(req_id):
request_id.set(req_id)
logger.info("Processing request")
5. 性能优化技巧
5.1 延迟日志评估
避免在日志调用时立即计算复杂表达式:
python复制# 错误做法(总会执行expensive_call())
logger.debug(f"Data: {expensive_call()}")
# 正确做法(使用%格式或延迟评估)
logger.debug("Data: %s", expensive_call())
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"Data: {expensive_call()}")
5.2 异步日志处理
使用QueueHandler实现非阻塞日志:
python复制from logging.handlers import QueueHandler, QueueListener
import queue
log_queue = queue.Queue(-1) # 无限队列
queue_handler = QueueHandler(log_queue)
logger.addHandler(queue_handler)
# 后台处理线程
listener = QueueListener(
log_queue,
logging.FileHandler("async.log"),
logging.StreamHandler()
)
listener.start()
6. 常见问题解决方案
6.1 日志重复输出问题
当出现重复日志时,检查:
- 是否多次调用basicConfig()
- 是否在多个地方添加了相同处理器
- 日志器的propagate属性是否为True(默认向上传播)
python复制# 禁止日志传播
logger.propagate = False
6.2 第三方库日志控制
python复制# 设置第三方库日志级别
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.INFO)
# 添加NullHandler避免"No handlers"警告
logging.getLogger("boto3").addHandler(logging.NullHandler())
7. 生产环境最佳实践
-
日志分级策略:
- DEBUG:开发环境详细日志
- INFO:生产环境运行指标
- WARNING:需要关注的异常
- ERROR:需要立即处理的问题
-
日志文件管理:
- 使用RotatingFileHandler/TimedRotatingFileHandler
- 每日压缩归档旧日志
- 设置合理的保留策略(如保留7天)
-
敏感信息过滤:
python复制class SensitiveDataFilter(logging.Filter):
def filter(self, record):
if "password" in record.msg.lower():
record.msg = "REDACTED - sensitive data"
return True
logger.addFilter(SensitiveDataFilter())
- 监控集成:
- 错误日志发送到Sentry/ELK
- 关键指标日志对接Prometheus
- 设置日志报警阈值
8. 从print迁移的实用技巧
- 全局替换print:
python复制def debug_print(*args, **kwargs):
import inspect
frame = inspect.currentframe().f_back
logger.debug(
"PRINT: %s",
" ".join(map(str, args)),
extra={"caller": frame.f_code.co_name}
)
# 重定向print
print = debug_print
- 保留控制台输出:
python复制class PrintLikeHandler(logging.StreamHandler):
def emit(self, record):
try:
msg = self.format(record)
print(msg) # 保持print风格但通过日志系统
self.flush()
except Exception:
self.handleError(record)
- 逐步迁移策略:
- 第一阶段:用logging.basicConfig()替换所有print
- 第二阶段:为不同模块配置独立logger
- 第三阶段:添加文件处理器和错误报警
- 第四阶段:实现结构化日志和监控集成
在实际项目中,我遇到过从print迁移到logging的典型挑战是处理多线程环境的日志混乱。解决方案是给每个线程配置独立的日志上下文,或者使用像loguru这样的第三方库来简化流程。关键是要记住:良好的日志实践应该像版本控制一样成为开发流程的核心部分,而不是事后补救措施。
