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模块的基础就这些。掌握了这些,你已经能应对大部分日常开发场景了。下一章我们会聊聊更高级的话题——如何把日志和链路追踪结合起来,实现真正的可观测性。