DRF-Tracking模块源码深度解析
一、drf-tracking模块简介
drf-tracking,简而言之,是一个专为Django REST Framework视图访问提供全面日志记录功能的第三方模块。它借助Mixin机制,几乎无缝地与DRF集成在一起,使用起来非常便捷。从源码结构来看,它同样是一个典型的Django应用(APP),内部定义了清晰的数据模型用于将日志持久化到数据库,并提供了自定义Manager来高效操作这些数据。模块的核心逻辑主要集中在base_mixins.py文件中。坦白说,该项目的代码体量与编码风格,对初学者而言是一份相当友好的学习资料。

二、基本使用方法与配置
安装流程与大多数Python模块相同,直接通过pip命令即可完成。
$ pip install drf-tracking
安装完毕后,需要将其注册到项目的INSTALLED_APPS中,应用名称为rest_framework_tracking。由于它依赖数据库表,因此还需执行一次数据库迁移操作。
$ python manage.py migrate
使用方式遵循Django一贯的面向对象继承哲学。只需让视图类继承LoggingMixin,默认情况下它会记录所有HTTP请求方法的日志。当然,你也可以按需筛选,仅对特定方法启用日志记录。
logging_methods = ['POST', 'PUT']
LoggingMixin提供了一个类属性logging_methods,类型为列表,用于指定需要记录的HTTP方法。通过阅读源码可以清晰看到,它利用should_log函数来判断是否记录日志,逻辑简洁明了,最终返回一个布尔值。
def should_log(self, request, response):
return self.logging_methods == '__all__' or request.method in self.logging_methods
这段代码的含义是:若logging_methods被设置为'__all__',则所有请求均被记录;否则,仅当请求方法存在于该列表中时才执行记录。你也可以重写此方法,根据业务场景定制自己的日志过滤规则,只要最终返回True或False即可。
那么should_log究竟在何处被调用?答案就在finalize_response方法中。该方法不仅负责调用should_log做决策,还负责组装完整的日志数据。
另外,LoggingMixin中还包含一个handle_log方法,它掌管着日志如何存储以及写入到何处。我们来查看一段核心代码。
def handle_log(self):
APIRequestLog(**self.log).sa ve()
APIRequestLog正是用于存储日志的模型表。这段代码的含义非常直观:实例化一个APIRequestLog对象,将self.log字典中的内容传递进去,最后调用sa ve()将其保存至数据库。至此,日志的存储方式与时机已清晰呈现,但还缺少一环:self.log字典究竟是如何构建起来的?
三、请求与响应钩子机制
熟悉DRF的开发者应该知道,DRF封装了Django原生的View,提供了APIView。在APIView中有一个initial方法,主要用于请求的预处理与校验。我们可以重写该方法,在请求抵达的第一时间初始化日志记录。drf-tracking正是采用了这一策略。
def initial(self, request, *args, **kwargs):
# 初始化我们需要的log字典
self.log = {}
# 封装请求体数据
self.log['requested_at'] = now()
self.log['data'] = self._clean_data(request.body)
super(BaseLoggingMixin, self).initial(request, *args, **kwargs)
try:
data = self.request.data.dict()
except AttributeError:
data = self.request.data
self.log['data'] = self._clean_data(data)
这里的关键在于super()的调用。它执行的是当前类MRO链中下一个类的initial方法,最终会回溯到APIView中。这是一种非常经典的“为函数扩展功能”的技巧——在父类方法执行前后插入自定义逻辑。
类似地,finalize_response也被重写了。我们来分析它的实现逻辑。
def finalize_response(self, request, response, *args, **kwargs):
# 先执行父类的finalize_response方法
response = super(BaseLoggingMixin, self).finalize_response(request, response, *args, **kwargs)
# 判断是否需要记录日志,这里兼顾了旧版本的_should_log钩子
should_log = self._should_log if hasattr(self, '_should_log') else self.should_log
if should_log(request, response):
# 处理响应内容
if response.streaming:
rendered_content = None
elif hasattr(response, 'rendered_content'):
rendered_content = response.rendered_content
else:
rendered_content = response.getvalue()
# 把各种信息打包进log字典
self.log.update({
'remote_addr': self._get_ip_address(request),
'view': self._get_view_name(request),
'view_method': self._get_view_method(request),
'path': request.path,
'host': request.get_host(),
'method': request.method,
'query_params': self._clean_data(request.query_params.dict()),
'user': self._get_user(request),
'response_ms': self._get_response_ms(),
'response': self._clean_data(rendered_content),
'status_code': response.status_code,
})
try:
# 调用handle_log来保存日志
if not connection.settings_dict.get('ATOMIC_REQUESTS'):
self.handle_log()
else:
if getattr(response, 'exception', None) and connection.in_atomic_block:
connection.set_rollback(True)
connection.set_rollback(False)
self.handle_log()
except Exception:
logger.exception('Logging API call raise exception!')
return response
至此,整个drf-tracking的核心源码脉络已基本打通。其本质就是重写DRF视图基类中的initial与finalize_response两个钩子方法,在请求进入与响应输出之际,悄无声息地记录下关键信息。值得一提的是,DRF本身也是采用同样的思路——通过重写Django的View来扩展功能的。
四、自定义扩展功能
第一次在GitHub上看到这个项目时,我大致浏览了其用法,第一印象是扩展性似乎不强,甚至觉得有些不够灵活。但后来沉下心来仔细研读源码(尤其是其重写思路),才发现它其实蕴含着不小的扩展潜力。下面分享我个人实践的自定义扩展示例。
注意:这种自定义方式下,我们无需在settings的INSTALLED_APPS中添加rest_framework_tracking。
自定义数据库模型
自带模型APIRequestLog仅提供基础字段。如果需要记录业务相关数据,就必须自行扩展。以下以告警需求为例,演示自定义Model。
class CustomApiLog(BaseAPIRequestLog):
subject = models.TextField(verbose_name='告警主题', default=None, null=True, blank=True)
sub_text = models.TextField(verbose_name='告警内容', default=None, null=True, blank=True)
我们新增了subject和sub_text两个字段,完全可以根据实际业务持续扩展。
自定义Mixin
前面提到,LoggingMixin提供的handle_log方法仅简单保存数据。我们可以重写此方法,在保存之前获取自定义字段的数据。具体需要完成两件事。
第一,在settings中指定自定义的Model。
LOG_MODEL = 'app.CustomApiLog'
第二,编写自定义的Mixin。
from rest_framework_tracking.base_mixins import BaseLoggingMixin
from rest_framework_tracking.base_models import BaseAPIRequestLog
from django.conf import settings
from django.apps import apps as django_apps
from django.core.exceptions import ImproperlyConfigured
def get_log_model():
try:
return django_apps.get_model(settings.LOG_MODEL, require_ready=False)
except ValueError:
raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
except LookupError:
raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.LOG_MODEL)
class CustomLoggingMixin(BaseLoggingMixin):
def _get_custom_fileds(self):
# 获取自定义的log表
self.CustomApiLog = get_log_model()
# 获取自定义的表字段
custom_filed = (item.name for item in set(self.CustomApiLog._meta.fields) - set(BaseAPIRequestLog._meta.fields))
# 更新log字典
for item in custom_filed:
if hasattr(self, 'get_%s' % item):
func = getattr(self, 'get_%s' % item)
result = func()
self.log.update({item: result})
def handle_log(self):
self._get_custom_fileds()
self.CustomApiLog(**self.log).sa ve()
自定义字段的值需要你编写对应的函数来提供。函数的命名约定为get_字段名。例如,要为subject字段赋值,就编写一个get_subject方法。
def get_subject(self):
return '【{user}】操作接口【{interface}】{operator}一条数据'.format(
user=self._get_user(self.request),
interface=self.request._request.path,
operator=self.method_dict.get(self.request.method.upper())
)
你完全可以重写我提供的这个自定义字段方法,在里面处理更复杂的业务逻辑,这完全不会影响日志记录的核心流程。
