1. Python日志系统概述
日志记录是任何成熟应用程序不可或缺的组成部分。Python内置的logging模块提供了一个灵活而强大的日志系统,可以满足从简单脚本到复杂分布式系统的各种日志需求。与简单的print语句相比,logging模块具有以下核心优势:
- 分级输出:支持DEBUG、INFO、WARNING、ERROR、CRITICAL五个标准日志级别
- 多目的地输出:可同时输出到控制台、文件、网络等不同目标
- 格式化控制:完全自定义日志输出的格式和内容
- 性能优化:相比print,logging在大量输出时性能更好
- 线程安全:内置线程安全机制,适合多线程环境
一个典型的日志系统初始化代码如下:
python复制import logging
# 创建logger实例
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建控制台handler并设置级别
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 创建文件handler并设置级别
fh = logging.FileHandler('app.log')
fh.setLevel(logging.DEBUG)
# 创建formatter并添加到handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# 将handler添加到logger
logger.addHandler(ch)
logger.addHandler(fh)
2. Logger模块核心组件详解
2.1 Logger对象
Logger是开发者直接交互的接口,每个logger都有一个名称,通常使用模块名称(如__name__)来命名,这样可以方便地追踪日志来源。Logger对象的主要职责包括:
- 提供日志记录方法(debug(), info(), warning(), error(), critical())
- 确定哪些日志需要处理(基于日志级别过滤)
- 将日志传递给所有关联的Handler
创建Logger的最佳实践:
python复制# 推荐使用模块名作为logger名称
logger = logging.getLogger(__name__)
# 避免在多个地方创建同名logger,应该获取已存在的logger
same_logger = logging.getLogger(__name__) # 这会返回同一个logger实例
2.2 Handler处理器
Handler负责将日志记录发送到适当的目的地。常见的Handler类型包括:
- StreamHandler:输出到流(通常是sys.stderr)
- FileHandler:输出到文件
- RotatingFileHandler:自动轮转的日志文件
- TimedRotatingFileHandler:按时间间隔轮转日志文件
- SMTPHandler:通过邮件发送日志
- SocketHandler:通过网络发送日志
配置多个Handler的示例:
python复制# 添加一个每天轮转的日志文件
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
'app.log',
when='midnight',
interval=1,
backupCount=7
)
handler.setLevel(logging.INFO)
logger.addHandler(handler)
2.3 Formatter格式化器
Formatter控制日志输出的格式,支持以下常用属性:
| 属性名 | 格式 | 描述 |
|---|---|---|
| asctime | %(asctime)s | 易读的时间格式 |
| name | %(name)s | Logger名称 |
| levelname | %(levelname)s | 日志级别名称 |
| message | %(message)s | 日志消息内容 |
| pathname | %(pathname)s | 调用日志的源文件完整路径 |
| lineno | %(lineno)d | 调用日志的代码行号 |
| funcName | %(funcName)s | 调用日志的函数名 |
高级格式化示例:
python复制formatter = logging.Formatter(
'[%(asctime)s] %(levelname)-8s %(name)-15s %(filename)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
3. 日志级别与过滤机制
3.1 标准日志级别
Python定义了5个标准日志级别,按严重性递增排列:
| 级别 | 数值 | 使用场景 |
|---|---|---|
| DEBUG | 10 | 详细的调试信息,通常只在开发时使用 |
| INFO | 20 | 确认程序按预期运行 |
| WARNING | 30 | 表明发生了意外情况,但程序仍能继续运行 |
| ERROR | 40 | 由于更严重的问题,某些功能无法正常工作 |
| CRITICAL | 50 | 严重错误,可能导致程序无法继续运行 |
设置日志级别有两条规则:
- Logger只会处理大于等于其设置级别的日志
- Handler只会处理大于等于其设置级别的日志
3.2 自定义日志级别
虽然不常见,但可以定义自己的日志级别:
python复制logging.addLevelName(25, 'VERBOSE')
logging.VERBOSE = 25
def verbose(self, message, *args, **kwargs):
if self.isEnabledFor(logging.VERBOSE):
self._log(logging.VERBOSE, message, args, kwargs)
logging.Logger.verbose = verbose
logger.verbose('This is a verbose message')
3.3 过滤器(Filter)机制
Filter提供了比日志级别更细粒度的控制,可以基于任何条件过滤日志记录:
python复制class ImportantFilter(logging.Filter):
def filter(self, record):
# 只允许包含"important"的消息通过
return 'important' in record.getMessage()
# 应用过滤器
important_filter = ImportantFilter()
logger.addFilter(important_filter)
4. 高级配置与最佳实践
4.1 配置文件方式配置
对于复杂项目,推荐使用配置文件(如JSON或YAML)来配置logging:
logging_config.json:
json复制{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.FileHandler",
"level": "DEBUG",
"formatter": "standard",
"filename": "app.log",
"encoding": "utf8"
}
},
"root": {
"level": "DEBUG",
"handlers": ["console", "file"]
}
}
加载配置:
python复制import logging.config
import json
with open('logging_config.json') as f:
config = json.load(f)
logging.config.dictConfig(config)
4.2 日志轮转策略
对于长期运行的服务,日志轮转是必须考虑的功能:
python复制from logging.handlers import RotatingFileHandler
# 每个日志文件最大10MB,保留5个备份
handler = RotatingFileHandler(
'app.log',
maxBytes=10*1024*1024,
backupCount=5
)
4.3 多模块日志管理
在大型项目中,合理的日志管理策略:
- 每个模块使用自己的logger(
logger = logging.getLogger(__name__)) - 在主模块中配置根logger
- 子模块logger会继承根logger的配置
- 可以为特定模块设置不同的日志级别
python复制# 设置特定模块的日志级别
logging.getLogger('requests').setLevel(logging.WARNING)
4.4 性能优化技巧
-
避免在日志调用前进行字符串格式化:
python复制# 不推荐 logger.debug('Value: %s' % expensive_func()) # 推荐 logger.debug('Value: %s', expensive_func()) -
使用isEnabledFor检查日志级别:
python复制if logger.isEnabledFor(logging.DEBUG): logger.debug('Message with %s', expensive_data()) -
对于高频日志,考虑使用内存Handler:
python复制from logging.handlers import MemoryHandler buffer_handler = MemoryHandler(capacity=100, target=real_handler) logger.addHandler(buffer_handler)
5. 常见问题与解决方案
5.1 日志重复输出问题
当看到同一条日志被多次输出时,通常是因为:
- 多次添加了相同的Handler
- 子logger和根logger都处理了同一条日志
解决方案:
python复制# 确保只添加一次Handler
if not logger.handlers:
logger.addHandler(handler)
# 或者设置propagate=False
logger.propagate = False
5.2 日志丢失问题
如果发现某些日志没有出现在输出中,检查:
- Logger和Handler的级别设置
- 是否有过滤器阻止了日志输出
- 是否在程序退出前调用了logging.shutdown()
5.3 多线程/多进程日志
- 多线程环境:logging模块默认是线程安全的
- 多进程环境:需要特殊处理,推荐使用QueueHandler和QueueListener
多进程日志示例:
python复制import logging
import logging.handlers
from multiprocessing import Queue
def listener_process(queue):
while True:
try:
record = queue.get()
if record is None:
break
logger = logging.getLogger(record.name)
logger.handle(record)
except Exception:
import traceback
print('Error in listener:', file=sys.stderr)
traceback.print_exc(file=sys.stderr)
queue = Queue()
listener = Process(target=listener_process, args=(queue,))
listener.start()
# 在工作进程中
handler = logging.handlers.QueueHandler(queue)
logger.addHandler(handler)
5.4 捕获异常堆栈
使用exc_info参数可以自动记录异常堆栈:
python复制try:
1/0
except Exception:
logger.error('Exception occurred', exc_info=True)
# 或者简写为
logger.exception('Exception occurred')
6. 实际应用案例
6.1 Flask应用日志配置
典型的Flask应用日志配置:
python复制from flask import Flask
import logging
from logging.handlers import RotatingFileHandler
app = Flask(__name__)
# 禁用Flask默认的logger
app.logger.handlers.clear()
# 配置根logger
handler = RotatingFileHandler('flask.log', maxBytes=10000, backupCount=1)
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
)
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
# 路由中使用日志
@app.route('/')
def index():
app.logger.info('Accessed index page')
return "Hello World"
6.2 Django日志配置
Django的settings.py中配置日志:
python复制LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': 'debug.log',
'formatter': 'verbose'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'include_html': True,
}
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
}
}
6.3 结构化日志实践
对于需要日志分析的场景,可以使用结构化日志:
python复制import logging
import json
class StructuredMessage:
def __init__(self, message, **kwargs):
self.message = message
self.kwargs = kwargs
def __str__(self):
return '%s >>> %s' % (self.message, json.dumps(self.kwargs))
_ = StructuredMessage
logger.info(_('User login', username='admin', ip='192.168.1.1', success=True))
7. 性能监控与诊断
7.1 使用日志进行性能分析
可以通过日志记录关键操作的执行时间:
python复制import time
import logging
from contextlib import contextmanager
@contextmanager
def log_time(description):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
logger.info('%s took %.3f seconds', description, elapsed)
# 使用示例
with log_time('Database query'):
# 执行数据库操作
time.sleep(0.5)
7.2 日志采样
对于高频日志,可以使用采样来减少日志量:
python复制import random
class SamplingFilter(logging.Filter):
def __init__(self, sample_rate=0.1):
self.sample_rate = sample_rate
def filter(self, record):
return random.random() < self.sample_rate
# 应用采样过滤器
logger.addFilter(SamplingFilter(0.1)) # 只记录10%的日志
7.3 日志与指标系统集成
可以将日志与Prometheus等指标系统集成:
python复制from prometheus_client import Counter
LOG_COUNTER = Counter('app_log_messages', 'Number of log messages', ['level'])
class MetricsFilter(logging.Filter):
def filter(self, record):
LOG_COUNTER.labels(level=record.levelname).inc()
return True
logger.addFilter(MetricsFilter())
