python logging模块:专业日志记录

从print转向logging,会发现一个强大得多的工具。logging模块不仅能替代print的功能,还提供了分级输出、灵活配置和持久化等强大特性。

1. 为什么用logging替代print?

特性 print logging
输出控制 只能全部开启/关闭 可分级控制(DEBUG, INFO等)
输出目的地 仅控制台 控制台、文件、网络等
信息丰富度 仅自定义内容 可包含时间、模块、行号等
性能影响 始终执行 可关闭低级别日志
生产环境适用性 优秀

为什么要用logging而不是print?

  1. 分级输出:logging有DEBUG, INFO, WARNING, ERROR, CRITICAL等不同级别,可以根据重要性分类日志。

  2. 控制输出:可以轻松地全局调整日志级别,只输出重要程度 above 某个级别的日志。

  3. 输出到多地方:可以同时将日志输出到控制台、文件、网络等。

  4. 包含更多信息:可以自动记录时间、模块名、行号等。

  5. 不会干扰正常输出:print会输出到标准输出,而logging可以输出到标准错误,并且可以分别控制。

如何使用logging?

基本步骤:

  1. 导入logging模块

  2. 配置logging(可选,但推荐)

  3. 使用logging来记录日志

2. logging的五个日志级别

复制代码
import logging

# 从低到高五个级别
logging.debug("调试信息")      # 最详细,用于调试
logging.info("普通信息")       # 确认程序按预期运行
logging.warning("警告信息")    # 表明意外情况,但程序仍运行
logging.error("错误信息")      # 严重问题,影响部分功能
logging.critical("严重错误")  # 致命错误,可能导致程序中断
  • 日志级别从低到高:DEBUG < INFO < WARNING < ERROR < CRITICAL
  • 设置一个级别后,只有该级别及以上的日志会被记录。所以,如果设置为INFO,那么DEBUG级别的日志不会输出,而INFO及以上的会输出。
  • 开发时设置为DEBUG级别,看到所有信息
  • 生产环境设置为WARNING级别,忽略调试信息

3. 基础使用方法

简单起步(替代print)

复制代码
import logging

# 基本配置(替换print语句)
logging.basicConfig(level=logging.INFO)

# 原来的print("开始处理数据")
logging.info("开始处理数据")

# 原来的print(f"用户{user_id}登录成功")
logging.info("用户%s登录成功", user_id)  # 更高效的格式化

# 原来的print("错误:文件不存在")
logging.error("文件不存在")

# 调试信息(原来可能用print来调试)
logging.debug("变量x的值:%s", x) # 该语句不会被输出

debug语句不输出是正确的、期望的行为!

这种设计的优势:

  1. 开发时 :设置level=DEBUG,看到所有细节

  2. 生产环境 :设置level=WARNING,只记录重要问题

  3. 无需修改代码:通过配置控制输出详细程度

4. 进阶配置:发挥logging真正威力

模块化使用(推荐)

复制代码
# 在每个模块中这样使用
import logging
logger = logging.getLogger(__name__)  # 使用模块名作为logger名称

class DataProcessor:
    def __init__(self):
        self.logger = logging.getLogger(__name__)
    
    def process(self, data):
        self.logger.debug("开始处理数据: %s", data[:100])  # 只记录前100字符
        
        if not data:
            self.logger.warning("接收到空数据")
            return None
            
        try:
            result = self._complex_operation(data)
            self.logger.info("数据处理成功,大小: %d", len(result))
            return result
        except ValueError as e:
            self.logger.error("数据格式错误: %s", e)
            raise
        except Exception as e:
            self.logger.critical("处理过程中发生意外错误", exc_info=True)
            raise
logger的两种方式:
复制代码
import logging

# 方式1:使用根logger(不推荐用于成熟项目)
logging.info("这是一条日志")  # 使用根logger

# 方式2:使用自定义logger(推荐)
logger = logging.getLogger(__name__)
logger.info("这是一条日志")  # 使用自定义logger

# 配置根logger(影响所有子logger)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

import logging

# 简单脚本可以这样用
logging.basicConfig(level=logging.INFO)
logging.info("程序启动")
logging.error("发生错误")

import logging

# 创建logger(通常以模块名命名)
logger = logging.getLogger(__name__)

# 配置logger
def setup_logging():
    handler = logging.StreamHandler()
    formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

setup_logging()

# 使用自定义logger
logger.info("程序启动")
logger.error("发生错误")
为什么使用自定义logger更好?

1. 更好的日志追踪

复制代码
# 使用根logger(难以追踪来源)
# 输出:INFO - 开始处理数据

# 使用自定义logger(清晰知道来自哪个模块)
# 输出:my_project.data_processor - INFO - 开始处理数据
# 输出:my_project.api - INFO - 接收API请求

2. 独立的配置控制

复制代码
import logging

# 为不同模块设置不同级别
data_logger = logging.getLogger('my_project.data_processor')
data_logger.setLevel(logging.DEBUG)  # 数据模块需要详细日志

api_logger = logging.getLogger('my_project.api') 
api_logger.setLevel(logging.WARNING)  # API模块只记录警告和错误

# 这样data_logger.debug()会输出,但api_logger.debug()不会

3. 模块化的日志管理

复制代码
# 主程序
import logging
from my_module import my_function
from another_module import another_function

# 配置根logger
logging.basicConfig(level=logging.INFO)

def main():
    logger = logging.getLogger(__name__)
    logger.info("主程序启动")
    
    # 这些函数内部使用它们自己的logger
    my_function()      # 使用my_module的logger
    another_function()  # 使用another_module的logger
    
    logger.info("主程序结束")

if __name__ == "__main__":
    main()

完整配置示例

复制代码
import logging
import sys

def setup_logging():
    # 创建logger
    logger = logging.getLogger('my_app')
    logger.setLevel(logging.DEBUG)  # 捕获所有级别日志
    
    # 避免重复添加handler
    if logger.handlers:
        return logger
    
    # 创建formatter
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
    )
    
    # 控制台handler(输出INFO及以上)
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)
    
    # 文件handler(输出DEBUG及以上,更详细)
    file_handler = logging.FileHandler('app.log', encoding='utf-8')
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)
    
    # 添加handler到logger
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    
    return logger

# 使用配置好的logger
logger = setup_logging()
处理器(handler)

我们使用logging模块时,可以设置多个处理器(handler),每个处理器可以设置不同的日志级别和输出目标(比如控制台、文件等)。这样,我们可以将不同级别的日志输出到不同的地方。

在上面的代码中,我们创建了两个处理器:

  1. 控制台处理器(StreamHandler):输出到标准输出(sys.stdout),级别为INFO,所以会处理INFO及以上的日志。

  2. 文件处理器(FileHandler):输出到文件'app.log',使用UTF-8编码,级别为DEBUG,所以会处理DEBUG及以上的日志。

然后,我们将这两个处理器添加到logger中。这样,当使用logger记录日志时,一条日志会同时被发送给这两个处理器(当然,每个处理器会根据自己设置的级别过滤,只有级别达到要求的日志才会被实际输出)。

例如,当我们调用logger.debug()时:

  • 控制台处理器级别为INFO,所以不会在控制台输出。

  • 文件处理器级别为DEBUG,所以会写入文件。

当我们调用logger.info()时:

  • 控制台处理器会输出到控制台。

  • 文件处理器会写入文件。

这样,我们就可以实现:

  • 在控制台上看到INFO及以上的日志(相对简洁)

  • 在文件中看到DEBUG及以上的日志(更详细,便于调试和问题追踪)

格式字符串
复制代码
# 创建formatter(信件格式)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')   

这个格式字符串定义了日志记录的输出格式。其中每个百分号加括号的部分是一个占位符,会被具体的日志信息替换。

格式字符串:'%(asctime)s - %(name)s - %(levelname)s - %(message)s'

各占位符含义:

  • %(asctime)s: 日志记录的时间,格式为字符串。默认格式是类似 '2024-01-15 10:30:00,000'。

  • %(name)s: 记录日志的logger的名称(通常是模块名,如 __name__)。

  • %(levelname)s: 日志级别的文本描述(如 'INFO', 'DEBUG'等)。

  • %(message)s: 日志消息内容。

例子:假设在模块 my_module中,有一条INFO级别的日志,消息是"用户登录成功",那么格式化后的日志可能像这样:

'2024-01-15 10:30:00,000 - my_module - INFO - 用户登录成功'

完整字段参考表

字段 说明 示例值
%(asctime)s 日志创建时间 2024-01-15 10:30:25,123
%(name)s logger名称 data_processor
%(levelname)s 日志级别 INFO, ERROR
%(message)s 日志消息 开始处理数据
%(filename)s 文件名 data_processor.py
%(pathname)s 完整文件路径 /home/user/project/data_processor.py
%(funcName)s 函数名 process_data
%(lineno)d 行号 45
%(module)s 模块名 data_processor
%(process)d 进程ID 1234
%(thread)d 线程ID 5678
%(threadName)s 线程名 MainThread

5. 实用技巧和最佳实践

性能优化

python 复制代码
# 使用判断避免不必要的字符串拼接
if logger.isEnabledFor(logging.DEBUG):
    logger.debug("详细数据: %s", expensive_to_compute_value())

# 而不是(性能差):
# logger.debug("详细数据: %s", expensive_to_compute_value())

在Python的logging模块中,即使日志消息不会被输出(因为当前设置的日志级别高于该日志语句的级别),在调用日志语句时,如果使用了字符串格式化操作,那么这些格式化操作仍然会执行,这可能会带来不必要的性能开销。

python 复制代码
import logging

# 假设这是一个计算成本很高的函数
def expensive_to_compute_value():
    # 模拟耗时操作:大数据处理、复杂计算、数据库查询等
    import time
    time.sleep(2)  # 模拟2秒的耗时计算
    return "复杂计算结果"

# 不推荐的写法(性能差)
logging.basicConfig(level=logging.INFO)  # 注意:级别是INFO,不是DEBUG

# 即使DEBUG日志不会输出,expensive_to_compute_value()也会被执行!
logger.debug("详细数据: %s", expensive_to_compute_value())

问题 :虽然日志级别是INFO,DEBUG消息不会输出,但expensive_to_compute_value()这个耗时函数仍然会被调用,浪费了2秒钟!

logger.isEnabledFor(level)方法用于检查当前logger是否会记录指定级别的日志

python 复制代码
import logging

logging.basicConfig(level=logging.INFO)  # 设置为INFO级别
logger = logging.getLogger(__name__)

# 检查DEBUG级别是否启用
print(f"DEBUG是否启用: {logger.isEnabledFor(logging.DEBUG)}")  # 输出: False
print(f"INFO是否启用: {logger.isEnabledFor(logging.INFO)}")    # 输出: True
print(f"WARNING是否启用: {logger.isEnabledFor(logging.WARNING)}")  # 输出: True

结构化日志记录(JSON格式)

复制代码
import json
import logging

class StructuredLogger:
    def __init__(self, name):
        self.logger = logging.getLogger(name)
    
    def log_transaction(self, user_id, action, status, details=None):
        log_data = {
            "timestamp": logging.getLogRecordFactory().time,
            "user_id": user_id,
            "action": action,
            "status": status,
            "details": details
        }
        self.logger.info(json.dumps(log_data))

# 使用
structured_logger = StructuredLogger(__name__)
structured_logger.log_transaction("user123", "login", "success")

根据不同环境配置

复制代码
def setup_environment_logging(env='development'):
    logger = logging.getLogger()
    
    if env == 'development':
        logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')
    elif env == 'production':
        logging.basicConfig(
            level=logging.WARNING,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('app.log'),
                logging.StreamHandler()
            ]
        )

6. 从print迁移的实用建议

  1. 逐步替换:不要一次性替换所有print,先替换重要的输出

  2. 保持习惯:继续用print做快速调试,但正式代码用logging

  3. 利用级别

    • 调试信息 → logging.debug()

    • 普通信息 → logging.info()

    • 警告信息 → logging.warning()

    • 错误信息 → logging.error()

相关推荐
追风少年ii2 小时前
脚本复习--高精度空转(Xenium、CosMx)的细胞邻域分析(R版本)
python·数据分析·空间·单细胞
AI科技星2 小时前
宇宙膨胀速度的光速极限:基于张祥前统一场论的第一性原理推导与观测验证
数据结构·人工智能·经验分享·python·算法·计算机视觉
秋刀奈2 小时前
基于 LangGraph 构建极简对话式 AI 智能体
python·langchain·agent
搞机械的假程序猿2 小时前
普中51单片机学习笔记-按键
笔记·学习·51单片机
重启编程之路2 小时前
python基础之进程学习
python
CodeLongBear2 小时前
MySQL进阶学习笔记:从单表查询到多表关联的深度解析(万字详解)
笔记·学习·mysql
程序员大雄学编程3 小时前
定积分的几何应用(一):平面图形面积计算详解
开发语言·python·数学·平面·微积分
小兵张健3 小时前
Java + Spring 到 Python + FastAPI (一)
java·python·spring
Element_南笙3 小时前
吴恩达新课程:Agentic AI(笔记6)
人工智能·笔记