logging模块 — 日志系统

Python标准库精讲专题 · 操作系统接口篇 · 掌握日志系统

专题:Python标准库精讲系统学习

关键词:Python, 标准库, logging, 日志, log, Logger, Handler, Formatter, 日志级别, 日志配置

一、日志系统概述

1.1 日志的重要性

日志是软件开发中不可或缺的组成部分。它记录了程序运行过程中的关键信息,帮助开发者理解系统行为、诊断问题、监控性能。与简单的 print() 语句相比,logging 模块提供了:日志级别控制(可按需输出不同详细程度的信息)、输出目标灵活(控制台、文件、网络等)、格式化输出(统一风格、包含时间戳等元信息)、运行时动态调整(无需重启即可改变日志行为)。

在生产环境中,日志往往是排查问题的第一手资料。良好的日志实践可以大幅降低故障排除的时间成本。一个没有日志的系统犹如一架没有仪表盘的飞机——一旦出现问题,几乎无从下手。

1.2 logging 模块架构

Python 的 logging 模块采用了模块化设计,由四大核心组件构成,它们协同工作完成了日志的完整生命周期。这四大组件分别是 Logger(日志器)、Handler(处理器)、Formatter(格式化器)和 Filter(过滤器)。

整个日志处理流程可以概括为:应用程序调用 Logger 记录日志 → Logger 根据日志级别判断是否处理 → Logger 将日志记录传递给绑定的 Handler → Handler 在输出之前经过 Filter 过滤 → Handler 使用 Formatter 将日志格式化为目标字符串 → Handler 将格式化后的日志输出到目标位置(控制台、文件等)。

四大组件关系图(文本示意):

Application Code │ ▼ ┌─────────────────────────────────────┐ │ Logger (日志器) │ │ - 检查日志级别是否满足阈值 │ │ - 创建 LogRecord 对象 │ │ - 传递给 Handler │ └──────────────┬──────────────────────┘ │ ┌──────────┴──────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Filter │ │ Filter │ │ (过滤器) │ │ (过滤器) │ └──────┬───────┘ └──────┬───────┘ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Handler │ │ Handler │ │ (处理器) │ │ (处理器) │ └──────┬───────┘ └──────┬───────┘ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Formatter │ │ Formatter │ │ (格式化器) │ │ (格式化器) │ └──────┬───────┘ └──────┬───────┘ │ │ ▼ ▼ 输出目标 输出目标 (控制台/文件/等) (控制台/文件/等)

1.3 LogRecord 对象

当日志被触发时,Logger 会创建一个 LogRecord 对象,其中封装了日志的所有信息,包括日志消息、级别、时间、调用位置、异常信息等。这个对象会沿着 Logger 的层级链传递,直到被处理。LogRecord 也是 Formatter 访问数据属性的来源。理解 LogRecord 的生命周期有助于开发者更好地控制日志输出行为。

二、日志级别

2.1 标准日志级别

logging 模块定义了五个标准日志级别,级别数值越低表示严重程度越高(从 DEBUG 到 CRITICAL 数值递增)。每个级别对应一个整数值,用于比较和过滤。

级别 数值 适用场景 示例
DEBUG 10 详细调试信息,仅在开发/诊断时有用 变量值、函数入口出口、SQL 语句
INFO 20 确认程序按预期运行 服务启动、请求到达、任务完成
WARNING 30 表示潜在问题,程序仍能运行 磁盘空间不足、已弃用 API 使用
ERROR 40 程序功能受阻,需要关注 数据库连接失败、文件不存在
CRITICAL 50 严重错误,程序可能无法继续 内存耗尽、关键服务不可用

2.2 setLevel 继承机制

每个 Logger 都有一个有效的日志级别。如果 Logger 上未显式设置级别,它会从父 Logger 继承。根 Logger 的默认级别是 WARNING。这意味着如果不对子 Logger 设置级别,只有 WARNING 及以上级别的日志才会被处理。

继承机制体现在两个层面:第一,日志级别的继承——子 Logger 如果没有设置级别,会向上查找父 Logger(最终到根 Logger)获取有效级别;第二,日志传播——Logger 处理的日志记录默认会传播给其父 Logger 的所有 Handler。这种层级设计使得日志管理非常灵活,可以在不同层级设置不同的日志策略。

关键理解点:setLevel 设置的是 Logger 的"门槛"——只有达到或超过该级别的日志才会被 Logger 接受并传递给 Handler。而 Handler 自身也有 setLevel,形成双重过滤机制。

import logging # 根 Logger 默认级别为 WARNING logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # 显式设为 DEBUG # 双重过滤:Logger 级别 + Handler 级别 handler = logging.StreamHandler() handler.setLevel(logging.INFO) # Handler 只处理 INFO 及以上 logger.addHandler(handler) logger.debug('这条不会输出') # Logger 允许,但 Handler 过滤掉了 logger.info('这条会输出') # 两级都通过

2.3 自定义日志级别

虽然五个标准级别已能满足绝大多数需求,但 logging 模块也支持自定义级别。自定义级别需要定义一个整数数值,并使用 logging.addLevelName() 注册名称。建议自定义级别时,数值避开标准级别的区间,以防冲突。

import logging # 自定义级别:TRACE = 5(低于 DEBUG) TRACE_LEVEL = 5 logging.addLevelName(TRACE_LEVEL, 'TRACE') def trace(self, message, *args, **kwargs): if self.isEnabledFor(TRACE_LEVEL): self._log(TRACE_LEVEL, message, args, **kwargs) logging.Logger.trace = trace logger = logging.getLogger('app') logger.setLevel(TRACE_LEVEL) handler = logging.StreamHandler() handler.setLevel(TRACE_LEVEL) logger.addHandler(handler) logger.trace('这是一条 TRACE 级别的日志')

需要特别注意:自定义级别虽然技术上可行,但在团队协作或开发库时,建议优先使用标准级别,以保证与其他代码的兼容性。自定义级别更适合用于特定应用的内部需求。

三、Logger 日志器

3.1 创建 Logger

Logger 对象的创建通过 logging.getLogger(name) 工厂函数完成,而非直接实例化 Logger 类。这个工厂函数会维护一个全局的 Logger 注册表,相同的 name 会返回同一个 Logger 实例。这是单例模式的应用,确保在整个应用中同一名称的 Logger 共享同一配置。

import logging # 根 Logger root_logger = logging.getLogger() # 模块级 Logger(推荐用法,使用 __name__) logger = logging.getLogger(__name__) # 具名 Logger logger = logging.getLogger('myapp.database') # 多次获取同一 Logger 返回同一实例 assert logging.getLogger('myapp') is logging.getLogger('myapp')

推荐在模块级别使用 __name__ 作为 Logger 名称,这样可以自动反映模块的包层级结构,便于日志管理和过滤。例如在 package.module 中调用 getLogger(__name__) 会得到名为 'package.module' 的 Logger。

3.2 层级命名

Logger 的名称使用点号分隔的层级命名空间,与 Python 包的层级结构一致。例如名为 'a' 的 Logger 是 'a.b' 的父 Logger。这种层级关系使得日志配置非常灵活——你可以统一配置顶层 Logger 的行为,然后为特定子 Logger 进行微调。

# 创建层级 Logger main = logging.getLogger('myapp') db = logging.getLogger('myapp.database') db_mysql = logging.getLogger('myapp.database.mysql') api = logging.getLogger('myapp.api') # 父 Logger 的 Handler 概念演示: # 如果 myapp 设置了一个 FileHandler, # 并且 logger 的 propagate 属性为 True(默认值), # 那么 myapp.database 的日志也会被 myapp 的 Handler 处理

重要概念 —— propagate 属性:

当 Logger 处理了一条日志后,它会检查自己的 propagate 属性。如果为 True(默认值),则该日志记录会继续传递给父 Logger 的 Handler 进行处理。这可能会导致日志重复输出——如果子 Logger 和父 Logger 都配置了相同的 Handler 类型。解决方案是将 propagate 设为 False,或确保 Handler 不重复添加。

3.3 basicConfig 快速配置

对于简单的脚本和快速原型开发,basicConfig() 提供了一行代码完成日志配置的便利方式。它一次性配置根 Logger 的格式、级别和输出目标。但要注意 basicConfig() 在根 Logger 第一次被配置后就失效了(除非设置 force=True)。

import logging # 最简单的配置:输出到控制台,级别 INFO logging.basicConfig(level=logging.INFO) # 完整配置示例 logging.basicConfig( level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', filename='app.log', # 输出到文件 filemode='a', # 追加模式 encoding='utf-8' ) # 多个 Handler(basicConfig 不支持,需改用更高级的方式) # basicConfig 只接受单个 filename,多个输出目标必须手动配置 Handler logger = logging.getLogger(__name__) logger.info('basicConfig 已经配置完成')

basicConfig 的限制:它只能配置根 Logger、只能添加一个 Handler、不支持多个输出目标、调用过一次后再次调用默认无效(Python 3.8+ 可传 force=True)。对于复杂应用,应使用 dictConfig 或手动配置。

四、Handler 处理器

4.1 Handler 基类与架构

Handler 负责将日志记录发送到目标位置。每个 Handler 都有自己的日志级别、格式化器和过滤器,这与 Logger 的配置相互独立。这种设计使得同一个 Logger 可以将同一日志记录同时输出到不同目标,且每个目标可以有不同的格式和级别策略。

常用的 Handler 有六种,它们继承自 logging.Handler 基类。此外,logging.handlers 模块还提供了更多高级 Handler,如基于队列的 QueueHandler、基于套接字的 SocketHandler 等。

4.2 StreamHandler

StreamHandler 将日志输出到任何实现了 write() 方法的流对象,默认是 sys.stderr。这是最常用的 Handler 之一,特别适合开发阶段和容器化部署场景。

import logging import sys logger = logging.getLogger('example') # 输出到标准输出(stdout) stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setLevel(logging.DEBUG) logger.addHandler(stdout_handler) # 输出到标准错误(stderr) stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setLevel(logging.WARNING) logger.addHandler(stderr_handler) # setFormatter 是可选的,默认使用 BASIC_FORMAT formatter = logging.Formatter('%(message)s') stdout_handler.setFormatter(formatter)

4.3 FileHandler

FileHandler 将日志写入文件。它支持指定文件名、写入模式和编码。默认模式为 'a'(追加),编码为平台默认编码。对于需要持久保存日志的场景,FileHandler 是最基本的选择。

import logging logger = logging.getLogger('example') # 基本文件 Handler file_handler = logging.FileHandler('app.log', mode='a', encoding='utf-8') file_handler.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) logger.addHandler(file_handler) # 每天生成一个日志文件(更实用的方案见 TimedRotatingFileHandler) logger.info('这条日志将写入 app.log')

4.4 RotatingFileHandler

RotatingFileHandler 在 FileHandler 的基础上增加了日志轮转功能。当日志文件达到指定大小时,会自动重命名并创建新文件。可以指定保留的备份文件数量,防止磁盘被日志填满。

import logging from logging.handlers import RotatingFileHandler logger = logging.getLogger('example') # 日志轮转:每个文件 5MB,保留 3 个备份 handler = RotatingFileHandler( 'app.log', maxBytes=5 * 1024 * 1024, # 5MB backupCount=3, encoding='utf-8' ) handler.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) # 当 app.log 达到 5MB,自动重命名为 app.log.1 # 新的日志继续写入 app.log # 保留 app.log, app.log.1, app.log.2, app.log.3

4.5 TimedRotatingFileHandler

TimedRotatingFileHandler 按时间间隔轮转日志,而非文件大小。支持按秒、分钟、小时、天、周或固定时间点轮转。适合需要按时间归档日志的场景(如每天一个日志文件)。

import logging from logging.handlers import TimedRotatingFileHandler logger = logging.getLogger('example') # 按天轮转,保留 7 天的日志 handler = TimedRotatingFileHandler( 'app.log', when='midnight', # 每天午夜轮转 interval=1, # 间隔 1 个单位 backupCount=7, # 保留 7 个备份 encoding='utf-8' ) handler.setLevel(logging.INFO) handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) # when 参数可选值: # 'S' - 秒, 'M' - 分钟, 'H' - 小时 # 'D' - 天, 'W0'-'W6' - 周(周一至周日) # 'midnight' - 每天午夜

4.6 SMTPHandler

SMTPHandler 可以将日志通过邮件发送,适合 ERROR 和 CRITICAL 级别的告警通知。配置时需要指定邮件服务器、收件人、发件人等信息。注意在生产环境中应谨慎使用,避免频繁发送邮件导致邮件服务器压力过大。

import logging from logging.handlers import SMTPHandler logger = logging.getLogger('example') # 邮件告警 Handler:仅发送 ERROR 及以上级别的日志 mail_handler = SMTPHandler( mailhost=('smtp.example.com', 587), fromaddr='logger@example.com', toaddrs=['admin@example.com', 'oncall@example.com'], subject='[APP ERROR] 生产环境错误告警', credentials=('user@example.com', 'password'), # 登录凭证 secure=() # 启用 TLS ) mail_handler.setLevel(logging.ERROR) formatter = logging.Formatter(''' 时间: %(asctime)s 级别: %(levelname)s 模块: %(name)s 位置: %(pathname)s:%(lineno)d 消息: %(message)s ''') mail_handler.setFormatter(formatter) logger.addHandler(mail_handler)

4.7 HTTPHandler

HTTPHandler 将日志通过 HTTP POST 或 GET 请求发送到远程服务器,适合集中式日志收集场景。结合 ELK(Elasticsearch, Logstash, Kibana)或 Grafana Loki 等日志平台,可以实现日志的集中管理和可视化分析。

import logging from logging.handlers import HTTPHandler logger = logging.getLogger('example') # 将日志发送到远程日志收集服务 http_handler = HTTPHandler( host='logs.example.com:8080', url='/api/logs', method='POST' # 或 'GET' ) http_handler.setLevel(logging.WARNING) logger.addHandler(http_handler) # 注意:HTTPHandler 默认发送的格式有限 # 更复杂的场景建议结合 QueueHandler + 后台线程异步发送

五、Formatter 格式化器

5.1 格式化字符串与 LogRecord 属性

Formatter 将 LogRecord 中的属性按照指定的模板格式化为字符串。格式化字符串使用 % 占位符风格,通过在 %()s 中引用 LogRecord 的属性名来实现。

以下是 LogRecord 对象的核心属性表:

属性名 格式占位符 说明
name %(name)s Logger 的名称
levelname %(levelname)s 日志级别的文本形式(如 'INFO')
levelno %(levelno)d 日志级别的整数值
pathname %(pathname)s 发出日志调用的源文件路径
filename %(filename)s pathname 的文件名部分
module %(module)s 模块名(filename 去掉 .py)
lineno %(lineno)d 发出日志调用的行号
funcName %(funcName)s 发出日志调用的函数名
created %(created)f LogRecord 创建时间(time.time() 返回值)
asctime %(asctime)s 格式化的可读时间字符串
msecs %(msecs)d 时间的毫秒部分
relativeCreated %(relativeCreated)d 相对于加载日志模块的时间(毫秒)
thread %(thread)d 线程 ID
threadName %(threadName)s 线程名称
process %(process)d 进程 ID
processName %(processName)s 进程名称
message %(message)s 日志消息(已应用参数替换后的结果)
exc_info (不直接使用) 异常元组 (type, value, traceback)
exc_text %(exc_text)s 异常信息的文本形式

5.2 日期格式

asctime 字段可以通过 Formatter 的 datefmt 参数自定义格式,格式语法与 time.strftime() 相同。如果未指定 datefmt,默认格式为 'YYYY-MM-DD HH:MM:SS,mmm'。

import logging # 自定义 Formatter formatter = logging.Formatter( fmt='%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # 输出示例: # 2026-05-05 23:59:00 [INFO ] myapp:42 - Server started on port 8080 # 2026-05-05 23:59:01 [WARNING ] myapp.database:128 - Connection pool is at 85% # 常见 datefmt 格式: # '%Y-%m-%d %H:%M:%S' → 2026-05-05 23:59:00 # '%m/%d/%Y %I:%M:%S %p' → 05/05/2026 11:59:00 PM # '%d %b %Y %H:%M:%S' → 05 May 2026 23:59:00 # '%Y-%m-%dT%H:%M:%S%z' → 2026-05-05T23:59:00+0800

5.3 自定义格式化

除了标准 Formatter,还可以通过继承 logging.Formatter 并重写 format() 方法实现完全自定义的输出格式。这在需要添加彩色输出、JSON 格式化输出、敏感信息脱敏等场景中非常有用。

import logging class ColoredFormatter(logging.Formatter): """为不同日志级别添加 ANSI 颜色""" COLORS = { 'DEBUG': '\033[36m', # 青色 'INFO': '\033[32m', # 绿色 'WARNING': '\033[33m', # 黄色 'ERROR': '\033[31m', # 红色 'CRITICAL': '\033[35m', # 紫色 } RESET = '\033[0m' def format(self, record): levelname = record.levelname color = self.COLORS.get(levelname, '') if color: record.levelname = f'{color}{levelname}{self.RESET}' return super().format(record) # 使用自定义 Formatter handler = logging.StreamHandler() handler.setFormatter(ColoredFormatter('%(levelname)s - %(message)s')) logger = logging.getLogger('example') logger.addHandler(handler) class JSONFormatter(logging.Formatter): """输出 JSON 格式的日志,便于日志分析系统处理""" def format(self, record): import json log_record = { 'timestamp': self.formatTime(record, self.datefmt), 'level': record.levelname, 'logger': record.name, 'message': record.getMessage(), 'module': record.module, 'line': record.lineno, } if record.exc_info and record.exc_info[0]: log_record['exception'] = self.formatException(record.exc_info) return json.dumps(log_record, ensure_ascii=False)

六、Filter 过滤器

6.1 基础 Filter

Filter 提供了更细粒度的日志控制,在级别过滤之上增加一层筛选。标准库的 logging.Filter 可以按照 Logger 名称前缀进行过滤。当 Filter 被添加到 Logger 上时,它会过滤掉所有来自非指定名称 Logger 的日志记录。

import logging logger = logging.getLogger('myapp') handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) # 仅允许 'myapp.database' 及其子 Logger 的日志通过 db_filter = logging.Filter('myapp.database') handler.addFilter(db_filter) logger.addHandler(handler) # 这个日志会被过滤掉(Logger 名是 'myapp',不是 'myapp.database') logger.info('这条日志不会输出') # 这个日志会通过(匹配前缀 'myapp.database') db_logger = logging.getLogger('myapp.database.mysql') db_logger.info('这条日志会输出')

6.2 自定义 Filter

通过继承 logging.Filter 并重写 filter() 方法,可以实现任意复杂的过滤逻辑。filter() 方法接收一个 LogRecord 对象,返回 True 表示放行,返回 False 表示拦截。自定义 Filter 适用于按用户 ID、请求追踪 ID、敏感信息脱敏等场景。

import logging class SensitiveDataFilter(logging.Filter): """过滤可能包含敏感信息的日志""" def __init__(self, patterns=None): super().__init__() self.patterns = patterns or ['password', 'secret', 'token', 'credit_card'] def filter(self, record): # 检查消息中是否包含敏感关键字 if hasattr(record, 'msg') and isinstance(record.msg, str): for pattern in self.patterns: if pattern in record.msg.lower(): return False # 拦截包含敏感信息的日志 return True class RequestIDFilter(logging.Filter): """为每条日志添加请求追踪 ID""" def filter(self, record): # 从线程局部变量或上下文变量中获取请求 ID import threading request_id = getattr(threading.current_thread(), 'request_id', 'N/A') record.request_id = request_id # 动态添加属性,Formatter 可引用 return True # 使用示例 handler = logging.StreamHandler() handler.addFilter(SensitiveDataFilter()) handler.addFilter(RequestIDFilter()) handler.setFormatter(logging.Formatter( '[%(request_id)s] %(levelname)s - %(message)s' )) logger = logging.getLogger('app') logger.addHandler(handler)

Filter 的执行时机:

Filter 可以在 Logger 级别添加(影响当前 Logger 及其子 Logger),也可以在 Handler 级别添加(仅影响该 Handler)。Logger 级别的 Filter 在日志被传播到父 Logger 之前执行;Handler 级别的 Filter 在 Handler 准备输出日志之前执行。合理利用这个特性可以实现不同粒度的日志控制。

七、日志配置方式

7.1 basicConfig 简单配置

basicConfig 适用于小型脚本和简单应用。它的优点是极简——一行代码即可让日志系统开始工作。缺点是灵活性不足,无法满足复杂需求。Python 3.8+ 新增了 force=True 参数,允许强制重新配置根 Logger。

import logging # 简单配置 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S', filename='app.log', filemode='a', encoding='utf-8' ) # 等价于手动配置: # root_logger = logging.getLogger() # handler = logging.FileHandler('app.log', 'a', 'utf-8') # handler.setFormatter(logging.Formatter('%(asctime)s - ...')) # handler.setLevel(logging.INFO) # root_logger.addHandler(handler)

7.2 dictConfig 字典配置

dictConfig 是 Python 3.2+ 引入的强大配置方式,通过一个嵌套字典完整描述日志系统配置。它支持配置多个 Logger、Handler、Formatter 和 Filter,以及它们之间的关联关系。这是生产环境中最推荐的配置方式。

import logging.config LOGGING_CONFIG = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'standard': { 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', }, 'detailed': { 'format': '%(asctime)s [%(levelname)s] %(pathname)s:%(lineno)d - %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', }, 'json': { '()': 'utils.logging.JSONFormatter', }, }, 'filters': { 'sensitive': { '()': 'utils.logging.SensitiveDataFilter', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'DEBUG', 'formatter': 'standard', 'stream': 'ext://sys.stdout', }, 'file': { 'class': 'logging.handlers.RotatingFileHandler', 'level': 'INFO', 'formatter': 'detailed', 'filename': 'logs/app.log', 'maxBytes': 10 * 1024 * 1024, 'backupCount': 5, 'encoding': 'utf-8', }, 'error_mail': { 'class': 'logging.handlers.SMTPHandler', 'level': 'ERROR', 'formatter': 'standard', 'mailhost': ('smtp.example.com', 587), 'fromaddr': 'logger@example.com', 'toaddrs': ['admin@example.com'], 'subject': '应用错误告警', }, }, 'loggers': { 'myapp': { 'handlers': ['console', 'file'], 'level': 'DEBUG', 'propagate': False, }, 'myapp.database': { 'handlers': ['console'], 'level': 'INFO', 'propagate': True, }, 'myapp.api': { 'handlers': ['console', 'error_mail'], 'level': 'WARNING', 'propagate': False, }, }, 'root': { 'handlers': ['console'], 'level': 'WARNING', }, } # 应用配置 logging.config.dictConfig(LOGGING_CONFIG) # 获取 Logger 并使用 logger = logging.getLogger('myapp') logger.info('应用启动成功')

7.3 fileConfig 文件配置

fileConfig 从 INI 格式的文件中读取日志配置。这种方式将配置与代码分离,修改日志配置无需修改代码。不过 INI 格式表达能力有限,不支持嵌套结构,复杂场景建议使用 dictConfig。

import logging.config # logging.ini 文件内容: ''' [loggers] keys=root,myapp [handlers] keys=consoleHandler,fileHandler [formatters] keys=standardFormatter [logger_root] level=WARNING handlers=consoleHandler [logger_myapp] level=DEBUG handlers=consoleHandler,fileHandler qualname=myapp propagate=0 [handler_consoleHandler] class=StreamHandler level=DEBUG formatter=standardFormatter args=(sys.stdout,) [handler_fileHandler] class=handlers.RotatingFileHandler level=INFO formatter=standardFormatter args=('app.log', 'a', 5242880, 5, 'utf-8') [formatter_standardFormatter] format=%(asctime)s [%(levelname)s] %(name)s: %(message)s datefmt=%Y-%m-%d %H:%M:%S ''' # 加载配置文件 logging.config.fileConfig('logging.ini') logger = logging.getLogger('myapp') logger.debug('配置文件方式已加载')

7.4 代码手动配置

通过代码直接创建和组装 Logger、Handler、Formatter 和 Filter 对象。这种方式最为灵活,适合需要动态调整配置的场景,比如根据环境变量、配置文件内容或数据库中的设置来构建日志系统。

import logging import sys def setup_logger(name: str, debug: bool = False) -> logging.Logger: """手动配置 Logger 的函数示例""" logger = logging.getLogger(name) # 避免重复添加 Handler if logger.handlers: return logger logger.setLevel(logging.DEBUG if debug else logging.INFO) # 控制台 Handler console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.DEBUG if debug else logging.INFO) console_handler.setFormatter(logging.Formatter( '%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S' )) # 文件 Handler(仅 ERROR 及以上) file_handler = logging.FileHandler('errors.log', encoding='utf-8') file_handler.setLevel(logging.ERROR) file_handler.setFormatter(logging.Formatter( '%(asctime)s [%(levelname)s] %(pathname)s:%(lineno)d - %(message)s' '\n%(exc_info)s' )) logger.addHandler(console_handler) logger.addHandler(file_handler) return logger # 使用示例 logger = setup_logger('myapp', debug=True) logger.info('应用启动') logger.error('数据库连接失败', exc_info=True)

八、最佳实践与总结

8.1 模块级别 Logger 模式

这是 Python 日志系统中最基础也最重要的实践:在每个模块的顶层创建 Logger,使用 __name__ 作为 Logger 名称。这种方式充分利用了 Python 的包层级机制,让你的日志自动带上模块名,便于追踪日志来源。

# myapp/database/connection.py import logging logger = logging.getLogger(__name__) # Logger 名称自动为 'myapp.database.connection' def connect(): logger.info('正在连接数据库...') try: # 连接逻辑 logger.debug('连接参数: host=%s, port=%d', host, port) except Exception as e: logger.error('数据库连接失败: %s', str(e)) raise

8.2 不要使用根 Logger 直接记录

在生产代码中,应避免直接使用 logging.info()、logging.debug() 等根 Logger 的便捷方法。这些方法使用根 Logger,无法在模块级别进行精细控制。始终使用模块级别的 Logger 实例。

# 不推荐 import logging logging.info('这条日志无法单独控制') # 使用根 Logger # 推荐 import logging logger = logging.getLogger(__name__) logger.info('这条日志可以精细控制')

8.3 避免日志重复

日志重复是最常见的问题之一。主要原因有:多次调用 basicConfig()(默认无效)、子 Logger 和父 Logger 的 propagate 导致双重输出、同一 Handler 被多次添加到同一 Logger。解决之道是在添加 Handler 前检查是否已有 Handler。

import logging def get_logger(name: str) -> logging.Logger: """安全获取 Logger,避免重复添加 Handler""" logger = logging.getLogger(name) # 如果已经配置过,直接返回 if logger.handlers: return logger # 首次配置 logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter( '%(asctime)s [%(levelname)s] %(name)s: %(message)s' )) logger.addHandler(handler) # 禁止向上传播到父 Logger logger.propagate = False return logger

8.4 日志轮转

在生产环境中,永远不要使用 FileHandler 而不设轮转。没有轮转的日志会无限增长,最终占满磁盘空间导致服务宕机。根据日志量和运维需求,选择合适的轮转策略:按大小或按时间。同时建议设置合理的 backupCount 控制磁盘使用量。

经验之谈:对于高流量的 Web 服务,建议同时使用按大小和按时间的轮转策略。按时间(如每天轮转)方便按日期归档和排查;按大小防止单个日志文件过大难以处理。另外,日志文件应放在单独的磁盘分区,避免日志爆满影响系统运行。

8.5 日志级别使用建议

级别 开发环境 测试环境 生产环境 使用建议
DEBUG 启用 按需 禁用 变量值、API 请求/响应、函数入口出口
INFO 启用 启用 启用 重要事件:服务启停、配置加载、定时任务执行
WARNING 启用 启用 启用 潜在问题:配置文件缺失(使用默认值)、API 降级
ERROR 启用 启用 启用 功能异常:数据库连接失败、第三方服务返回错误
CRITICAL 启用 启用 启用 系统级故障:磁盘写满、内存不足、服务不可用

8.6 性能注意事项

日志操作虽然开销不大,但在高并发场景下仍然需要注意性能。以下是一些优化建议:使用日志参数延迟格式化(即使用 logger.debug('msg %s', var) 而非 logger.debug(f'msg {var}')),避免日志级别未达到时仍执行字符串格式化;使用 QueueHandler + QueueListener 实现异步日志处理,将日志 I/O 操作移到后台线程;对于高频调用的代码路径,先用 isEnabledFor() 检查级别再执行日志语句。

import logging from logging.handlers import QueueHandler, QueueListener import queue # 异步日志:将日志写入队列,后台线程处理 log_queue = queue.Queue(-1) queue_handler = QueueHandler(log_queue) logger = logging.getLogger('async_app') logger.addHandler(queue_handler) # 实际的 Handler(文件写入) file_handler = logging.FileHandler('async_app.log', encoding='utf-8') file_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s')) # 监听器在后台线程中处理队列中的日志 listener = QueueListener(log_queue, file_handler) listener.start() # 此时日志写入是异步的,不会阻塞主线程 for i in range(10000): logger.info(f'异步日志第 %d 条', i) # 程序退出前停止监听器 listener.stop()

8.7 总结

Python 的 logging 模块虽然学习曲线相对陡峭,但一旦掌握了它的架构和设计思想,就会发现它极其灵活和强大。通过合理使用四大组件(Logger、Handler、Formatter、Filter),结合适当的配置方式(dictConfig 是首选),可以构建出满足任何需求的日志系统。

核心要点速记:

1. 每个模块创建自己的 Logger:logger = logging.getLogger(__name__)

2. 日志级别双重过滤:Logger 级别 + Handler 级别

3. dictConfig 是生产环境的首选配置方式

4. 务必使用日志轮转(RotatingFileHandler / TimedRotatingFileHandler)

5. propagate=False 避免日志重复传播

6. 使用 Logger 的 .exception() 自动记录异常堆栈

7. 延迟格式化提升性能:logger.info('msg %s', var) 而非 f-string

8. 生产环境按 ERROR 级别邮件告警