诚然,每个人都会写bug,程序抛异常是一件很正常的事;既然异常总是会抛,那就想办法在抛出后,尽早解决才是王道。就拿Django来说,通常发生未知异常时,我们会将settings里的DEBUG=False改为True,然后盯着日志看。可谁没事老盯着日志看啊,未免也太浪费时间了;不能老是等待用户反馈异常和问题,万一用户懒得反馈了,岂不很尴尬。
需求:在异常发生时,进行异常埋点,接入开源平台Cat.
本篇是【Sentry部署+DingDing告警+Django接入】的兄弟篇,感兴趣可以了解下。
思路
-
Django自定义异常处理
-
Django中间件针对异常做处理
-
Django集成
1、Django自定义异常处理
得赖于DRF对Django深度贴合,DRF中也有类似的处理过程。这里对官方文档做进一步总结,具体使用姿势可移步官网。
DRF异常处理的思路:dispatch 处理请求 -> handle_exception -> exception_handler -> raise_uncaught_exception -> raise
- 在APIView类里有个方法:handle_exception,它是在APIView处理视图方法,发生异常时调用
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
- 这个handle_exception主要干了这么一件事,这里看看源码
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses, else coerce to 403
auth_header = self.get_authenticate_header(self.request)
if auth_header:
exc.auth_header = auth_header
else:
exc.status_code = status.HTTP_403_FORBIDDEN
exception_handler = self.settings.EXCEPTION_HANDLER
context = self.get_exception_handler_context()
response = exception_handler(exc, context)
if response is None:
self.raise_uncaught_exception(exc)
response.exception = True
return response
可以看到,handle_exception拿到view层发生的异常后,将异常转换为特定状态码的response,并添加特定的说明信息,如果转换不了,就直接抛出异常。raise_uncaught_exception就是在settings.DEBUG为True时,打印异常相关的日志、堆栈。也就是说,如果我们要对未捕获的异常做埋点,就可以放到raise_uncaught_exception这里。但这里也仅限于view层的异常,其他处理过程中的异常,这里无法处理。综上:方案一(自定义异常处理)不是一种好办法。
2、Django中间件针对异常做处理
中间件是一个轻量级、底层的插件系统,可以介入 django 的请求和响应处理过程,修改 django 的输入和输出。其各个处理阶段,可参考这篇博文:Django中间件讲解 。文中同样提到某些场景下的异常,同样不能捕获,所以通过中间件也不是一种好办法。不过中间件+猴子补丁,或许可以达到想要的效果。例如针对django.core.handlers.exception文件里的response_for_exception方法做补丁替换,该方法本人已在生产中使用,效果颇佳。
- 我们来看看,一个Django请求,在中间件里,是如何流转的:
- 一个简单的中间件使用案例:
import cat
import time
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpRequest, HttpResponse
from comm.utils import generate_request_id
class MonitorMiddleware(MiddlewareMixin):
def __init__(self, get_response=None):
super(MonitorMiddleware, self).__init__(get_response)
@staticmethod
def process_exception(request, exc):
# type: (HttpRequest, Exception) -> None
"""
处理所有的异常, 在views.py中出现异常时被调用,返回None或时HttpResponse对象, 注意:404错误属于url的异常,这里不能被捕捉到
:param request: 进入的请求对象
:param exc: 异常对象
:return:
"""
cat.init("my-application-name", debug=False, encoder=cat.ENCODER_TEXT, sampling=True, logview=False)
with cat.Transaction(request.get_full_path(), request.method) as t:
t.add_data(request.get_full_path(), request.META.get('HTTP_REFERER'))
cat.metric(u'{0}_{1}'.format(request.method, request.get_full_path())).count()
cat.log_exception(exc)
return
@staticmethod
def process_request(request):
# type: (HttpRequest) -> None
"""
在urls.py之前调用,返回None或HttpResponse对象
:param request:
:return:
"""
request.context = dict(
start_time=int(time.time()),
request_id=generate_request_id(request)
)
@staticmethod
def process_response(request, response):
# type: (HttpRequest, HttpResponse) -> None
"""
在模板渲染完,返回浏览器前调用, 返回 HttpResponse 或 StreamingHttpResponse 对象
:param request:
:param response:
:return:
"""
context = getattr(request, 'context', {})
end_time = int(time.time())
pass
@staticmethod
def process_view(request, view_func, view_args, view_kwargs):
"""
在views.py之前调用,返回None或时HttpResponse对象
:param request:
:param view_func:
:param view_args:
:param view_kwargs:
:return:
"""
pass
@staticmethod
def process_template_response(request, response):
"""
views.py 之后,渲染模板之前执行 返回实现了render方法的响应对象
:param self:
:param request:
:param response:
:return:
"""
pass
3、Django集成
灵感来源:sentry对Django异常的采集思路,但是我们需要将sentry的集成放到我们的项目里,以便完成对其扩展,加入我们的异常埋点代码,同时又不影响sentry的接入。这种方式难度较大,改sentry源码很可能改脏,不熟悉python的话,就不建议这种方式了。