Python标准库中的
1 | logging |
模块是每个生产级应用不可或缺的基础设施。然而,大量开发者对它的使用仍停留在
1 | print() |
和
1 | logging.basicConfig() |
的初级阶段。本文将深入剖析 logging 模块的架构设计,并通过实战案例演示如何构建结构化日志、日志轮转、多模块协作以及分布式追踪等高级场景。

一、logging 模块的架构设计
Python 的 logging 模块采用了经典的四层架构,理解这四层之间的关系是掌握日志系统的基础:
- Logger(记录器):应用程序直接调用的接口,负责产生日志事件
- Handler(处理器):决定日志输出到哪里(文件、控制台、网络等)
- Filter(过滤器):提供比级别更细粒度的日志过滤能力
- Formatter(格式化器):决定日志记录的最终输出格式
这四者的数据流关系为:Logger 产生 LogRecord → 经过 Filter 筛选 → 交给 Handler → Handler 再经过自身 Filter 筛选 → Formatter 格式化 → 最终输出。一条日志记录在到达最终输出之前,会经过多层过滤,这种设计赋予了 logging 模块极强的灵活性。
日志级别的正确理解
logging 定义了六个标准级别,从低到高依次为:
| 级别 | 数值 | 适用场景 |
|---|---|---|
| DEBUG | 10 | 调试信息,仅在开发环境启用 |
| INFO | 20 | 确认程序按预期运行 |
| WARNING | 30 | 表明发生了意外,但程序仍可运行 |
| ERROR | 40 | 由于严重问题,某些功能无法执行 |
| CRITICAL | 50 | 程序本身可能无法继续运行 |
一个常见的误区是认为级别越高越重要,实际上级别反映的是「严重程度」而非「重要性」。DEBUG 级别在排查问题时同样关键,只是在生产环境中需要关闭以减少日志量和性能开销。
二、基础配置与最佳实践
避免 basicConfig 的陷阱
1 | logging.basicConfig() |
是入门最简单的方式,但它有一个关键的陷阱:该函数只有在 root logger 没有配置任何 handler 时才会生效。如果你在其他模块中先调用了
1 | logging.info() |
(这会自动为 root logger 添加一个 StreamHandler),那么后续的 basicConfig 调用将完全无效。
作为 logger 名称,这样日志的层级结构会自然地反映模块的导入路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 import logging
# 错误示范:先调用 info 会导致 basicConfig 失效
logging.info('这条会触发默认 handler 的创建')
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 上面的 basicConfig 不会生效,因为 root logger 已经有 handler 了
# 正确做法:先配置,再使用
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.info('配置已生效')</pre>
<h3>模块级 Logger 的命名规范</h3>
在生产代码中,永远不要直接使用 root logger,而应该为每个模块创建独立的 logger。最佳实践是使用 <code>__name__。这个机制既能让你集中处理所有日志,也容易导致重复输出的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 # myapp/services/user_service.py
import logging
logger = logging.getLogger(__name__)
# logger 名称为 'myapp.services.user_service'
logger.info('用户服务已启动')</pre>
通过这种方式,你可以在主应用中按模块名精确控制日志级别。例如,只调试用户服务模块而不影响其他模块:
<pre><code>logging.getLogger('myapp.services.user_service').setLevel(logging.DEBUG)
logging.getLogger('myapp').setLevel(logging.INFO)</pre>
logger 的层级传播机制意味着,子 logger 产生的日志记录会自动传播给父 logger 的 handler,除非你设置了 <code>logger.propagate = False三、Handler 与日志轮转实战
![]()
文件日志的轮转策略
生产环境中最常见的日志输出目标是文件。但如果不做轮转处理,日志文件会无限增长,最终耗尽磁盘空间。logging 模块提供了两个轮转处理器: RotatingFileHandler — 按文件大小轮转:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
'/var/log/myapp/app.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5, # 保留5个历史文件
encoding='utf-8'
)
handler.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s'
))
logger = logging.getLogger('myapp')
logger.setLevel(logging.INFO)
logger.addHandler(handler)TimedRotatingFileHandler — 按时间轮转:
1
2
3
4
5
6
7
8
9
10 from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
'/var/log/myapp/app.log',
when='midnight', # 每天午夜轮转
interval=1,
backupCount=30, # 保留30天的日志
encoding='utf-8'
)
handler.suffix = '%Y-%m-%d.log' # 轮转文件的后缀格式
1 when参数支持的值包括:
1 S(秒)、
1 M(分)、
1 H(小时)、
1 D(天)、
1 midnight(午夜)、
1 W0-W6(每周指定星期)。选择哪种轮转策略取决于应用日志的产生速度和保留需求。
多 Handler 组合
实际应用中通常需要同时输出到控制台和文件,甚至同时输出到多个不同级别的文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 import logging
def setup_logging():
logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)
# 控制台输出 INFO 及以上
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s'
))
logger.addHandler(console)
# 文件输出 DEBUG 及以上(完整日志)
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler(
'/var/log/myapp/debug.log',
maxBytes=50 * 1024 * 1024,
backupCount=5,
encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s:%(funcName)s:%(lineno)d - %(message)s'
))
logger.addHandler(file_handler)
# 错误日志单独输出
error_handler = RotatingFileHandler(
'/var/log/myapp/error.log',
maxBytes=10 * 1024 * 1024,
backupCount=10,
encoding='utf-8'
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s - %(message)s'
))
logger.addHandler(error_handler)
return logger四、结构化日志:JSON 格式输出
在微服务和容器化环境中,传统纯文本日志难以被日志聚合系统(如 ELK Stack、Loki)有效解析。结构化日志(Structured Logging)以 JSON 格式输出,每个字段都可以被独立索引和查询,这是现代日志体系的基石。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
'timestamp': datetime.utcfromtimestamp(record.created).isoformat() + 'Z',
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno,
}
# 合并 extra 字段
if hasattr(record, 'extra_data'):
log_entry.update(record.extra_data)
# 异常信息
if record.exc_info:
log_entry['exception'] = self.formatException(record.exc_info)
return json.dumps(log_entry, ensure_ascii=False)
# 使用示例
logger = logging.getLogger('myapp')
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info('用户登录', extra={
'extra_data': {
'user_id': 12345,
'ip_address': '192.168.1.100',
'user_agent': 'Mozilla/5.0'
}
})上述代码输出的日志为:
1 {"timestamp": "2026-09-04T12:00:00Z", "level": "INFO", "logger": "myapp", "message": "用户登录", "module": "main", "function": "<module>", "line": 25, "user_id": 12345, "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0"}在生产环境中,推荐直接使用
1 python-json-logger库,它提供了更完善的 JSON 格式化能力,支持自定义字段添加和异常处理。
五、日志上下文与请求追踪
使用 LoggerAdapter 添加上下文
在 Web 应用中,每条日志最好携带当前请求的上下文信息(如请求 ID、用户 ID)。
1 LoggerAdapter提供了一种简单的方式:
模块提供了更好的方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import logging
class RequestContextAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
context = self.extra or {}
request_id = context.get('request_id', 'N/A')
user_id = context.get('user_id', 'N/A')
return f'[{request_id}] [user:{user_id}] {msg}', kwargs
logger = logging.getLogger('myapp')
base_logger = logging.getLogger('myapp')
# 在请求处理中
request_logger = RequestContextAdapter(base_logger, {
'request_id': 'req-abc-123',
'user_id': 42
})
request_logger.info('处理订单创建请求')</pre>
<h3>使用 contextvars 实现线程安全的上下文</h3>
LoggerAdapter 需要手动传递,在异步框架中不太方便。Python 3.7+ 的 <code>contextvars
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 import logging
import contextvars
request_id_var = contextvars.ContextVar('request_id', default='-')
user_id_var = contextvars.ContextVar('user_id', default='-')
class ContextFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id_var.get()
record.user_id = user_id_var.get()
return True
# 配置 logger
logger = logging.getLogger('myapp')
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
'%(asctime)s [%(request_id)s] [user:%(user_id)s] [%(levelname)s] %(message)s'
))
handler.addFilter(ContextFilter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# 在 ASGI/WSGI 中间件中设置上下文
async def request_middleware(request, call_next):
rid = request.headers.get('X-Request-ID', generate_id())
token1 = request_id_var.set(rid)
token2 = user_id_var.set(get_user_id(request))
try:
response = await call_next(request)
logger.info('请求处理完成')
return response
finally:
request_id_var.reset(token1)
user_id_var.reset(token2)这种方案在 asyncio 环境下也能正确工作,因为
1 contextvars的上下文会在每个 Task 中自动隔离,不会出现请求间上下文串号的问题。
六、日志性能优化
延迟格式化与条件判断
一个常见但容易被忽视的性能问题是日志消息的字符串格式化。即使日志级别不满足输出条件,以下代码的字符串拼接操作仍然会执行:
1
2
3
4
5
6
7
8
9 # 不推荐:字符串拼接总会执行
logger.debug(f'处理数据: {large_object_to_serialize()}')
# 推荐:使用 % 风格的延迟格式化
logger.debug('处理数据: %s', large_object_to_serialize())
# 更好的方案:先判断级别
if logger.isEnabledFor(logging.DEBUG):
logger.debug('处理数据: %s', expensive_serialize(large_object))
1 logging模块使用
1 %格式化风格是有意为之的——只有当日志真的要输出时,才会执行格式化操作。而 f-string 和
1 str.format()在调用
1 logger.debug()时就已经完成了字符串构造,造成不必要的性能损耗。
QueueHandler 与异步日志
在高并发场景下,多个线程同时写日志可能导致 I/O 竞争。
1 QueueHandler和
1 QueueListener提供了线程安全的异步日志方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 import logging
import logging.handlers
import queue
log_queue = queue.Queue(-1) # 无限队列
# QueueHandler 放在线程中,将日志放入队列
queue_handler = logging.handlers.QueueHandler(log_queue)
# QueueListener 在单独线程中从队列取日志,交给真正的 handler
file_handler = logging.handlers.RotatingFileHandler(
'/var/log/myapp/app.log', maxBytes=100*1024*1024, backupCount=5
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s - %(message)s'
))
listener = logging.handlers.QueueListener(log_queue, file_handler)
listener.start()
logger = logging.getLogger('myapp')
logger.addHandler(queue_handler)
logger.setLevel(logging.INFO)
# 应用退出时
import atexit
atexit.register(listener.stop)这种架构下,业务线程只做入队操作(极快),实际的文件 I/O 由 listener 线程负责,完全解耦了日志输出与业务逻辑的性能影响。
七、配置管理:dictConfig 实战
当项目规模增长,简单的代码配置会变得难以维护。
1 logging.config.dictConfig()允许你用字典(通常来自 YAML 文件)集中管理所有日志配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 version: 1
formatters:
standard:
format: '%(asctime)s [%(levelname)s] %(name)s - %(message)s'
datefmt: '%Y-%m-%d %H:%M:%S'
json:
class: 'pythonjsonlogger.jsonlogger.JsonFormatter'
format: '%(asctime)s %(levelname)s %(name)s %(message)s'
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: standard
stream: ext://sys.stdout
file:
class: logging.handlers.RotatingFileHandler
level: DEBUG
formatter: standard
filename: /var/log/myapp/app.log
maxBytes: 52428800
backupCount: 5
encoding: utf-8
error_file:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: standard
filename: /var/log/myapp/error.log
maxBytes: 10485760
backupCount: 10
encoding: utf-8
loggers:
myapp:
level: DEBUG
handlers: [console, file, error_file]
propagate: false
myapp.services:
level: INFO
root:
level: WARNING
handlers: [console]</pre>
1
2
3
4
5
6
7
8 import logging.config
import yaml
with open('logging_config.yaml') as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)
logger = logging.getLogger('myapp')这种方式的优势在于:配置与代码解耦,不同环境(开发、测试、生产)可以使用不同的配置文件,且无需修改任何业务代码。
八、与 Sentry 等错误追踪系统集成
对于生产应用,仅靠文件日志往往不够。Sentry 等错误追踪平台可以自动聚合异常、通知团队并提供堆栈分析。集成方式非常简单——添加一个自定义 Handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import logging
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
sentry_logging = LoggingIntegration(
level=logging.INFO, # INFO 及以上发送到 Sentry
event_level=logging.ERROR # ERROR 及以上创建 event
)
sentry_sdk.init(
dsn='https://your-dsn@sentry.io/project-id',
integrations=[sentry_logging],
environment='production',
traces_sample_rate=0.1
)
logger = logging.getLogger('myapp')
logger.error('数据库连接失败', extra={
'extra': {'db_host': 'db.prod.internal', 'db_port': 5432}
})这样,所有 ERROR 级别以上的日志都会自动上报到 Sentry,包含完整的堆栈信息和上下文,极大缩短了问题排查时间。
总结
Python logging 模块虽然 API 设计略显繁琐,但其四层架构提供了极强的灵活性。回顾本文的核心要点:
- 使用
1__name__
为每个模块创建独立 logger,利用层级传播机制集中管理
- 生产环境必须使用 RotatingFileHandler 或 TimedRotatingFileHandler 做日志轮转
- 采用 JSON 结构化日志输出,方便 ELK/Loki 等系统聚合分析
- 使用 contextvars 在异步环境中安全传递请求上下文
- 通过 QueueHandler 实现异步日志,避免 I/O 竞争影响业务性能
- 用 dictConfig + YAML 文件实现配置与代码解耦
日志系统是应用可观测性的基石。投入时间构建一套完善的日志体系,将在生产排障时带来数十倍的回报。日志不是事后补丁,而是架构设计的一部分。
汤不热吧
