供应链开发第六天
# 项目结构
基本涵盖了常见的项目结构
项目结构的划分分为二个步骤, app划分好后进行每个app里视图的划分.
# app
三种 我们的供应链项目采用的是第三种Hhh.
□ 项目里就只有一个app.(app里放的是与自己相关的数据表、视图、静态文件等)
□ 项目里有多个app ==> 多个业务app(每个app里放的是与自己相关的数据表、视图、静态文件等)
□ 项目里有多个app ==> 一个只放数据表的app+多个业务app(每个app里放的是与自己相关的视图、静态文件等)
如何考虑的? 若数据表没那么容易划分到各自的app里,杂糅在一起的,那么我们就不划分表,让表定义在一块!!
上面阐述的第二种情况是前提, 在项目开发过程中不可避免的会涉及到多个app中表结构的关联,如何解决呢? 详看下图:
注: UserInfo和Company表虽在不同的app里,迁移后都会放到了同一个数据库里!! 生成的表名是 所在app名字_ORM表名小写
.
# 视图
三种 我们的供应链项目采用的是第二种Hhh.
□ views.py : 当前app的所有的业务都写在这一个view.py视图文件里面
□ views文件夹 + 多个视图文件 : 对当前app的所有业务进行拆分,拆分成多个视图文件,每个视图文件中指定的实现一些业务!
□ views文件夹 + 多个业务文件夹 : 让我们更加的关注业务!!
直接上图!!哈哈哈 下方截图的例子,里面都是多个app (¯﹃¯),无伤大雅
针对第三种情况, 我们来思考下, 公共的模块, 即utils文件夹 怎么放?
(以上面截图中的注释 " 从'2'如何变为'3' " 的内容为例)
1> 最细粒度的,放在具体的某个业务里, 其他地方用不到 apps/shipper/views/account/utils
2> 放在views文件夹下, 该app中的所有业务都可以用到 apps/shipper/views/utils
3> 放到项目根目录,即与apps文件夹的同级目录下, 表明所有app都可以用到!!
# 返回值 - 自定义mixins
将视图中的
mixins
相关的视图重写, 业务视图继承自己视图, 对于各种情况的返回值进行定制
截图中, perform_create是对应新增的接口的
根据截图中的perform_create的业务逻辑代码,得重写对应的CreateModelMixin类
class CreateModelMixin:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return Response({"code": ret.FIELD_ERROR, "detail": serializer.errors})
res = self.perform_create(serializer)
return res or Response({"code": ret.SUCCESS, 'data': serializer.data})
def perform_create(self, serializer):
serializer.save()
2
3
4
5
6
7
8
9
10
11
12
□ 正确数据相关的返回值 eg: {"code":0,data:数据}
□ 错误数据相关的返回值.
1> 自定义的错误 eg:对象不存在、业务逻辑上不满足(比如,余额不足).
2> 内置组件"认证、权限、限流等"的错误
# 测试准备
按照上面的第三种视图结构写了个小Demo!! 利用drf快速的实现了一个接口的CURD!!
▲ 准备工作
- 利用pychram创建python项目 项目名为dbhot
- 在终端输入命令
pip install django==3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install djangorestframework -i https://pypi.tuna.tsinghua.edu.cn/simple
django-admin startproject dbhot .
python manage.py stratapp api
- 在pycharm中进行项目快速启动的配置
- 进行app的注册 'rest_framework','api.apps.ApiConfig',
- 设计ORM表、路由分发、快速实现多个接口
http://127.0.0.1:8000/api/app01/account/demo/
GET请求 查看列表
POST请求 添加一条数据
http://127.0.0.1:8000/api/app01/account/demo/1/
GET请求 查看某一条数据
DELETE请求 删除某一条数据
PUT请求 修改某一条数据
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
效果: 可以用drf提供的页面进行CURD!!
★关键代码如下: (这个代码结构我超喜欢!!)
# 返回值 - 自定义异常
需要分析源码后, 重写exception_handler方法!!
★ 重写后,就无需自定义mixins对异常设置相应的返回值,而使用drf原生的mixins就可实现对所有的异常进行处理!
前者是主动设置返回值,后者是统一在最后进行处理时设置返回值!!
# 异常默认返回
通过上面准备工作中实现的这个接口,进行测试,模拟出 程序一系列的异常 , 看看默认返回的数据是什么样的!!
依次列举常见的5种异常默认的数据返回的格式
# 0> 字段自身验证出错
字段自身验证出错.
HTTP 400 Bad Request
,{"age": ["A valid integer is required."]}
,raise ValidationError
因为此示例中,ORM模型中就规定了年龄age字段必须是整型.
所以在新增数据时,若请求体里携带的age字段的值是字符串,就会抛出异常 raise ValidationError (errors)
◆ 如何求证?
找那张自己画的 序列化验证相关的源码图, 就知道内部抛出的是 raise ValidationError (errors)
2
3
4
5
# 1> 访问数据不存在
访问的数据不存在.
HTTP 404 Not Found
,{"detail": "Not found."}
,raise Http404
已知: UserInfo表中 ID为100的记录是不存在的!
实验:
http://127.0.0.1:8000/api/app01/account/demo/100/ GET请求
drf的源码 self.get_object() 这一行代码会报错!!
回顾下为什么?
self.get_object() 本质上是在执行ORM语句 models.UserInfo.objects.get(id=100) 来获取数据
该ORM语句没找到或者找到多条都会报错!! raise Http404
self.get_object()处的源码会捕获该错误,抛出一个http404的异常,抛出的这个异常又会被dispatch捕获!!
◆ 如何求证?
找self.get_object()的源码 - get_object_or_404 - raise Http404 - disptch - handle_exception
▲ 以前我们是咋 玩/解决 的?
class RetrieveModelMixin:
def retrieve(self, request, *args, **kwargs):
try:
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response({"code": ret.SUCCESS, "data": serializer.data})
except Http404 as e:
return Response({"code": ret.NOT_FOUND, "msg": "对象不存在!"})
except Exception as e:
return Response({"code": ret.SUMMARY_ERROR, "msg": "获取该条数据失败!"})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2> 认证组件不通过
认证组件不通过,
HTTP 401 Unauthorized
,{"detail": "认证失败"}
,raise exceptions.AuthenticationFailed
主动伪造一个认证失败的情景.
无论CURD哪个操作,都通过不了,认证组件会抛出一个AuthenticationFailed类型的异常,抛出的这个异常又会被dispatch捕获!!
◆ 如何求证?
你看我们写的代码,认证组件不通过 raise exceptions.AuthenticationFailed("认证失败")
然后看 那张自己画的 认证权限 相关的源码图, 左下角就有这个异常类的源码.
▲ 以前我们是咋 玩/解决 的?
raise exceptions.AuthenticationFailed("认证失败") -- {"detail": "认证失败"}
raise exceptions.AuthenticationFailed({"code": 1000, "msg": "认证失败"}) -- {"code": 1000, "msg": "认证失败"}
2
3
4
5
6
7
8
9
10
# 3> 权限组件不通过
权限组件不通过
HTTP 403 Forbidden
,{"detail": "Authentication credentials were not provided."}
raise exceptions.NotAuthenticated()
这里只伪造了权限组件抛出raise exceptions.NotAuthenticated()的情况.
# 注: NotAuthenticated异常类源码的code是401的,dispatch那做了处理!!
它还可以抛出raise exceptions.PermissionDenied()
详看权限章节源码的解读!!
◆ 如何求证?
看 那张自己画的 认证权限 相关的源码图, 中间最下面就是可能抛出的两个异常,结合该章节的源码解读能获知什么情况下抛出什么异常.
看 那张自己画的 认证权限 相关的源码图, 左下角就有这两个异常类的源码.
2
3
4
5
6
7
8
# 4> 限流组件不通过
限流组件不通过.
HTTP 429 Too Many Requests
,{"detail": "Request was throttled."}
,raise exceptions.Throttled
限流不通过,源码中会主动抛出 raise exceptions.Throttled(wait)
详看限流章节源码的解读!!
◆ 如何求证?
看 那张自己画的 限流 相关的源码图,右下角就是异常类Throttled
2
3
4
5
# 源码解析
不禁想问, 上述的这些异常/报错 是谁处理的呢? 它们是怎样做的返回值? 这就得品一品异常相关的源码啦!!
- 字段自身验证出错. {"age": ["A valid integer is required."]}
HTTP 400 Bad Request,
raise ValidationError
- 访问的数据不存在. {"detail": "Not found."}
HTTP 404 Not Found
raise Http404
- 认证组件不通过. {"detail": "认证失败"}
HTTP 401 Unauthorized
raise exceptions.AuthenticationFailed
# 一定要注意,AuthenticationFailed异常类的源码的code是401
# 若认证类重写了authenticate_header方法,那么就保持401,否则会变成403 (在APIview的源码handle_exception方法处可以得到验证.
- 权限组件不通过
■ {"detail": "Authentication credentials were not provided."}
HTTP 403 Forbidden
raise exceptions.NotAuthenticated()
# NotAuthenticated异常类源码的code是401的,但dispatch那做了处理,401变成了403!!
# 在APIview的源码handle_exception方法处可以得到验证. 关键在于先前执行的认证类是否重写了handle_exception方法!!
■ {"detail": "You do not have permission to perform this action."}
HTTP 403 Forbidden
raise exceptions.PermissionDenied()
- 限流组件不通过. {"detail": "Request was throttled."}
HTTP 429 Too Many Requests
raise exceptions.Throttled
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 自定义exception_handler
★ 重写该方法后,就无需自定义mixins对异常设置相应的返回值,而使用drf原生的mixins就可实现对所有的异常进行处理!
前者是主动设置返回值,后者是统一在最后进行处理时设置返回值!!
# 5异常+APIException
捕获 5个常见的异常 + 继承了APIException的异常
关键代码截图如下: (注意哦,示例中使用的是drf自己的ModelViewSet
再特地粘贴下,重写的exception_handler的代码!
from django.http import Http404
from rest_framework import exceptions
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.exceptions import NotAuthenticated
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import Throttled
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.views import set_rollback
def exception_handler(exc, context):
# ■ 处理常见的这5个异常
# 给这些异常都加了个属性 ret_code !!
if isinstance(exc, Http404): # 获取不到
exc = exceptions.NotFound()
exc.ret_code = 1001
elif isinstance(exc, PermissionDenied): # 权限不通过
exc = exceptions.PermissionDenied()
exc.ret_code = 1002
elif isinstance(exc, (AuthenticationFailed, NotAuthenticated)): # 未认证、认证不通过
exc.ret_code = 1003
elif isinstance(exc, Throttled): # 限流
exc.ret_code = 1004
elif isinstance(exc, ValidationError): # 字段自身校验出错的
exc.ret_code = 1005
# ■ 处理drf相关的其他异常(前提是:该异常继承了APIException
# 我们在utils.exceptions里写自定义的ExtraException异常也是可以被捕获到的
if isinstance(exc, exceptions.APIException):
# 这部分跟源码一样
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)):
data = exc.detail
# 这部分相较与源码做了点更改!
else:
# 等同于 code = getattr(exc, 'ret_code', None) or -1
code = getattr(exc, 'ret_code', -1)
data = {'code': code, 'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
return None # ▲ 像python级别的Exception异常是处理不了的!!
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
46
47
48
# 全部异常
捕获 5个常见的异常 + 继承了APIException的异常 + python级别的Exception异常
关键代码截图如下: (注意哦,示例中使用的是drf自己的ModelViewSet
再特地粘贴下,重写的exception_handler的代码!
from django.http import Http404
from rest_framework import exceptions
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.exceptions import NotAuthenticated
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import Throttled
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.views import set_rollback
# 还添加了对Exception异常的捕获!!至此,所有的异常都可进行处理啦!!! ψ(`∇´)ψ
def exception_handler2(exc, context):
if isinstance(exc, Http404):
exc = exceptions.NotFound()
exc.ret_code = 1001
elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied()
exc.ret_code = 1002
elif isinstance(exc, (AuthenticationFailed, NotAuthenticated)):
exc.ret_code = 1003
elif isinstance(exc, Throttled):
exc.ret_code = 1004
elif isinstance(exc, ValidationError):
exc.ret_code = 1005
# APIException和Exception都会执行下面5行代码
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc, exceptions.APIException): # APIException
detail = exc.detail
else: # Exception
# detail = "请求异常"
detail = str(exc) # eg: "invalid literal for int() with base 10: 'hello'"
if isinstance(detail, (list, dict)):
data = detail
else:
code = getattr(exc, 'ret_code', -1)
data = {'code': code, 'detail': detail}
set_rollback()
return Response(data, headers=headers) # 有了code, 就无需status=exc.status_code参数啦
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
46
47
# 返回值 - 正常返回值
通过前面的归纳总结, 我们知道了可自定义mixins对正常返回值的格式进行处理..
当然我们可以不用自定义mixins, 而是通过其他方式实现, 下面将提供 两个方案!!
你看 本章节测试准备小节gif动图中 呈现的正常数据的返回格式是不符合规范的, 不是我们想要的!! 它没有code值, 如何进一步处理呢?
# 方案一: 重写finalize_response
关键在于 重写视图类的 finalize_response方法!!
其实将重写的finalize_response方法放在 示例中的DemoView类里 也是可以的!!Hhhh
# 方案二: 中间件
所有的response的返回, 都是要先经过中间件的!! 所以我们可以考虑在中间件里进行处理!!
# 事务
Django中提供了几个级别的事务
Ps: redis有事务 但redis的事务不支持回滚; sql的事务是支持回滚的!!
# 局部事务*
也可称作是 代码块级别的 事务!!
# 基本操作
基于上下文管理, 代码块中如果出现异常则自动回滚, 无异常则自动提交.
示例代码如下:
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from api import models
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
# 避免代码执行过程中出现异常导致报错带来不好的体验,用try..except..对异常进行了捕获!!
try:
with transaction.atomic():
# 这两个数据库的操作,要么一起成功,要么一起失败!!代码块中出现异常或者数据库操作失败都会自动进行回滚
models.UserInfo.objects.create(name='v1', age=1)
models.Order.objects.create(name='v1', age=1)
int("asdf") # 有异常,会回滚
except Exception as e:
print("代码块中出现异常,异常导致失败,会自动回滚")
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 回调函数
事务提交的回调函数 (本质上就是事务完成后, 自动执行一个函数) !!
但要注意, 回调函数执行过程中出现异常, 是不会进行回滚的! 因为它跟事务的内容无关.
示例代码如下: "几乎不会用到,了解即可"
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from api import models
# 偏函数,简单理解就是 把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单
from functools import partial
def db_success_callback(*args, **kwargs):
print(args, kwargs)
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
try:
with transaction.atomic():
# 回调函数,当事务正常提交后会自动执行
# transaction.on_commit(db_success_callback)
transaction.on_commit(partial(db_success_callback, 11, 22, 33)) # 11,22,33会被*args接收
models.UserInfo.objects.create(name='v1', age=1)
models.Order.objects.create(title='v1', count=1)
except Exception as e:
print("异常,自动回滚") # on_commit回调函数内部出现异常是不会回滚的!
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 回滚点
回滚到指定的事务点
应用场景:
事务里有很多操作,操作执行完成后,我通过逻辑的判断,发现某个操作及该操作后面的操作都不需要提交了
只需提交该操作之前的那些操作, 这就得用到 回滚点的知识!!
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from api import models
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
try:
with transaction.atomic():
n1 = transaction.savepoint() # 操作点n1
models.UserInfo.objects.create(name="qq3", age=21)
models.UserInfo.objects.create(name="qq4", age=21)
n2 = transaction.savepoint() # 操作点n2
models.UserInfo.objects.create(name="qq5", age=21)
# 经过一系列的逻辑判断,我想回滚到指定事务点,该事务点后的sql操作都不会提交
# 注:必须在事务里面
transaction.savepoint_rollback(n2) # 该示例中,qq3、qq4会提交,qq5不会提交
# PS: transaction.savepoint_rollback(n1) # 这样的话,示例中的sql操作都提交不了.
except Exception as e:
print("异常,自动回滚", e)
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 视图事务
视图级别的事务
这个也用的少,更多情况下我们是想更细粒度的某几个操作是基于事务的, 而不是整体基于事务
# 基本操作
★ 视图内,数据库操作有异常,会自动回滚 ; 视图内,有其他异常,不会回滚.
示例代码如下:
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from api import models
class Demo1View(APIView):
@transaction.atomic
def get(self, request, *args, **kwargs):
try:
models.UserInfo.objects.create(name='v100', age=1)
models.UserInfo.objects.create(name="v200", age="xxx") # 有异常,自动回滚,即: v100不会保存
int("asdf") # 有异常,不会回滚,即:两条数据正常保存到数据库
except Exception as e:
pass
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
需要注意一点, 如果在视图函数上使用 @transaction.atomic
不好使, 那么请在 dispatch方法上使用该装饰器!!
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from api import models
from django.utils.decorators import method_decorator
@method_decorator(transaction.atomic, name='dispatch')
class Demo1View(APIView):
@transaction.atomic # 这也意味着,视图函数get、post、put等都加上了事务.
def dispatch(self, request, *args, **kwargs):
return super().disptch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
try:
models.UserInfo.objects.create(name='v100', age=1)
models.UserInfo.objects.create(name="v200", age="xxx")
int("asdf")
except Exception as e:
pass
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果你觉得在dispatch上加事务,上面的写法比较麻烦. 还可以像下面这样写.
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from api import models
from django.utils.decorators import method_decorator # (¯﹃¯)
@method_decorator(transaction.atomic, name='dispatch') # (¯﹃¯)
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
try:
models.UserInfo.objects.create(name='v100', age=1)
models.UserInfo.objects.create(name="v200", age="xxx")
int("asdf")
except Exception as e:
pass
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 回滚点
定义事务点, 自定义回滚位置.
示例代码如下:
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from api import models
class Demo1View(APIView):
@transaction.atomic
def get(self, request, *args, **kwargs):
try:
models.UserInfo.objects.create(name='v10', age=1)
n1 = transaction.savepoint()
models.UserInfo.objects.create(name="v11", age=1)
n2 = transaction.savepoint()
models.UserInfo.objects.create(name='v12', age=1)
n3 = transaction.savepoint()
models.UserInfo.objects.create(name='v13', age=1)
# 后续读取到某些值后,发现 v11、v12、v13不应该创建,那么就可以主动回滚
transaction.savepoint_rollback(n1)
except Exception as e:
print("有异常", e)
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 全局事务
也称作 请求级别的事务, 每个请求都会加上事务
效率低, 项目中一般不会使用.
如果想要开启全局事务, 需要在连接数据库时多设置一个参数:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'dbhot4',
'USER': 'root',
'PASSWORD': 'root123',
'HOST': '127.0.0.1',
'PORT': '3306',
# (¯﹃¯)你可以借此看下APIView中exception_handler方法中的有关事务回滚的代码set_rollback()
# 其源码就读取了该值!!
'ATOMIC_REQUESTS': True
}
}
2
3
4
5
6
7
8
9
10
11
12
13
1> 只要视图函数执行异常, 无论是什么原因触发, 均自动回滚.
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
models.UserInfo.objects.create(name='v1', age=1)
models.UserInfo.objects.create(xxxxxxx='v2', age=1) # 错误
return Response("Hello World")
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
models.UserInfo.objects.create(name='v1', age=1)
models.UserInfo.objects.create(name='v2', age=1)
int("asdf") # 错误
return Response("Hello World")
2
3
4
5
6
7
8
9
10
11
12
2> 如果视图函数执行不报错 (try..except..将异常捕获了, 也算作不报错), 则不会回滚
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
try:
models.UserInfo.objects.create(name='v1', age=1)
models.UserInfo.objects.create(xxxxxxx='v2', age=1)
int("xxx")
except Exception as e:
pass
return Response("...")
# 视图函数执行没有报错,不会滚回.
2
3
4
5
6
7
8
9
10
11
3> 如果开启了全局事务, 想要免除某个指定的函数不需要开启事务, 则可以使用
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction, IntegrityError
from api import models
from django.utils.decorators import method_decorator # (¯﹃¯)
@method_decorator(transaction.non_atomic_requests, name='dispatch') # (¯﹃¯)
class Demo1View(APIView):
def get(self, request, *args, **kwargs):
models.UserInfo.objects.create(name='v100', age=1)
models.UserInfo.objects.create(name="v200", age="xxx") # 报错
return Response("...")
2
3
4
5
6
7
8
9
10
11
12
13
# Logging日志
python内置的logging模块 (Django、python脚本皆可使用)
When to use logs?
1> 已预知的情况.
2> 不可预知的情况. 比如,连接数据库时,数据库莫名断开了;网络出现异常连接不上了.
我们可以自己搞文件操作来记录日志吗?
可以,但需解决多线程同时写入时的线程安全问题!! 而logging模块默认就是线程安全的!!
应用场景举例
1> 发送短信需要与短信供应商对账, 但通常不会将发送短信记录存储到数据库中, 所以会使用日志.
2> 用户的支付情况、客户线索
3> 错误的日志. 那么每个错误的地方都要写日志语句吗? 不用, 前面我们不是 自定义了exception_handler吗! 在这里写!!
日志很多很多, 查日志就不方便了, 往后就涉及到 ELK、centry了.. 需要构建一些上层服务, 运维相关,暂略!!
问题: 遇到多进程会有问题,特定情况下删除日志会有问题!
优化: 使用centry;进行源码定制解决多进程的问题!
"""
★ --日志格式
"""
%(name)s Logger的名字
%(levelno)s 数字形式的日志级别
%(levelname)s 文本形式的日志级别
%(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
%(filename)s 调用日志输出函数的模块的文件名
%(module)s 调用日志输出函数的模块名
%(funcName)s 调用日志输出函数的函数名
%(lineno)d 调用日志输出函数的语句所在的代码行
%(created)f 当前时间,用UNIX标准表示时间,浮点数表示
%(relativeCreated)d 输出日志信息时的,自Logger创建以来的毫秒数
%(asctime)s 字符串形式的当前时间. 默认格式是 "2003-07-08 16:49:45,896".逗号后面的是毫秒
%(thread)d 线程ID,可能没有
%(threadName)s 线程名,可能没有
%(process)d 进程ID,可能没有
%(message)s 用户输出的消息
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 快速使用
注意哦, 快速使用的代码,是无法将同一日志写入多个地方(文件/屏幕)中的!
import logging
# 1.通过logging.basicConfig对日志进行配置
logging.basicConfig(
filename='error.log', # 日志文件的名字, 允许程序后,该文件会自动创建!
format='%(asctime)s : %(message)s', # 将日志写入文件时,日志呈现的格式
datefmt='%Y-%m-%d %H:%M:%S %p', # 指定时间的格式,若不指定,默认是这样的 2024-02-19 15:32:01,889
level=20 # 只有当日志的级别大于等于20时,该日志才会被写入到文件中.
)
# 2.日志写入文件
logging.log(20, "Hello World!")
logging.debug("debug级别是10")
logging.info("info的级别是20")
logging.error("error的级别是40")
"""
2024-02-19 17:11:09 PM : Hello World!
2024-02-19 17:11:09 PM : info的级别是20
2024-02-19 17:11:09 PM : error的级别是40
"""
""" 敲黑板
像该示例中这样快速的使用,是无法将日志写入多个文件中的!
在一个进程中,若已经通过logging.basicConfig对日志进行了配置,后面的代码再通过logging.basicConfig进行的配置是不会生效的!!
"""
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
# 对象版本
使用对象版本的日志代码, 是可以将同一日志写入多个地方(文件/屏幕)中的!
对象版本不常用!
★ -- Formatter: 控制日志格式
★ -- Handler: 负责日志输出的目标 在该示例中,维护了2个文件+1个屏幕.
★ -- handler对象.setFormatter(formatter对象)
▲ 用于绑定handler对象与formatter对象.
★ -- Logger: 负责产生日志信息,可定义级别.
★ -- Logger对象.addHandler(handler对象)
▲ 用于绑定logger对象与handler对象
import logging
formatter1 = logging.Formatter(
# 2024-02-19 16:56:54 PM - 交易日志 - INFO - v2: 小明给了小红100
fmt='%(asctime)s - %(name)s - %(levelname)s - %(module)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S %p',
)
h1 = logging.FileHandler(filename='a1.log', mode='a', encoding='utf-8') # -- 文件,默认就是a模式
h1.setFormatter(formatter1)
formatter2 = logging.Formatter(
# 2024-02-19 16:56:54: 小明给了小红100
fmt='%(asctime)s: %(message)s',
datefmt='%Y-%m-%d %X',
)
h2 = logging.FileHandler(filename='a2.log', encoding='utf-8') # -- 文件,默认就是a模式
h2.setFormatter(formatter2)
sm = logging.StreamHandler() # -- 屏幕
sm.setFormatter(formatter2)
"""
日志级别有5档: NOTSET -> DEBUG -> INFO -> WARNING/WARN -> ERROR -> CRITICAL/FATAL
分别对应数字: 无0 - 调试10 - 消息20 - 警告30 - 错误40 - 严重50
"""
# 若设置日志级别为error,那么会记录error和critical的信息
logger_obj = logging.Logger('交易日志', level=logging.INFO) # 此处'交易日志'是Logger日志的名字
# ◇★■ 此处该logger_obj绑定了多个handler,意味着同一个日志可输出到绑定的多个handler各自设定的输出目标中!!
logger_obj.addHandler(h1)
logger_obj.addHandler(h2)
logger_obj.addHandler(sm)
logger_obj.info('小明给了小红1000')
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
# 配置版本
配置版本是常见常用的!!
配置 | 含义 |
---|---|
loggers | 产生日志的对象 |
filters | 过滤日志的对象 |
handlers | 接收日志然后控制打印到不同的地方 FileHandler用来打印到文件中,StreamHandler用来打印到终端 |
formatters | 可以定制不同的日志格式对象,然后绑定给不同的Handler对象使用 以此来控制不同的Handler的日志格式 |
# 示例1: 基本配置
进行最基本的配置
运行该文件后, 2024-02-19 20:22:08 PM INFO MainThread : 6666666666
会写入demo.log
文件中, 并在终端显示..
import logging.config
LOGGING_CONFIG = {
"version": 1, # 日志的版本号,随意写
# 是否删除已存在其他日志的Handler / 是否保留Django内部已经存在的日志处理 -- 保留False,禁止True
# eg: Django内部本身就配置了一些日志,所以当我们访问某个接口时,能在终端看到访问的信息
"disable_existing_loggers": True,
# 支持在formatters中写多个formatter formatter的名字是自己可以随便起的
'formatters': {
'standard': {
'format': '{asctime} {levelname} {threadName} : {message}',
'style': '{', # 表明占位符是"{"
"datefmt": '%Y-%m-%d %H:%M:%S %p', # 日期的格式
},
'simple': {
'format': '%(asctime)s %(levelname)s %(message)s',
'style': '%', # 表明占位符是"%"
"datefmt": '%Y-%m-%d',
},
},
# 支持在handlers中写多个handler handler的名字是自己可以随便起的
'handlers': {
'console': {
'class': 'logging.StreamHandler', # 表明写到终端中
'formatter': 'standard', # handler需与formatter进行绑定,表明用啥格式
},
'demo': {
# 查看from logging.handlers import RotatingFileHandler的源码可得知
# filename、maxBytes、backupCount、encoding 都是 RotatingFileHandler初始化的参数!!
"class": 'logging.handlers.RotatingFileHandler', # 表明写到文件中
'formatter': 'standard',
'filename': 'demo.log', # 日志写在哪个文件中
# 'maxBytes': 1024, # 根据文件大小拆分日志,若经过日积月累后文件的大小超过1024个字节,会再新建一个文件写后续的日志
# 'backupCount': 5, # 结合maxBytes的值,意味着此示例中只会保留 最新的 5*1024 字节的日志
"encoding": "utf-8"
}
},
# 支持在loggers中写多个logger对象 logger的名字是可以自己随便起的
'loggers': {
'nb': {
'handlers': ['console', 'demo'], # 即log数据既写入文件又打印到屏幕
'level': "INFO", # >=20 则触发日志
'propagate': True # ★ 表明是否触发父级日志/根logger的配置?!
}
},
# 根logger的配置,就是所谓的父级日志.
'root': {
'handlers': ['console', ],
'level': 'ERROR',
'propagate': True
}
}
if __name__ == '__main__':
# 导入上面定义的logging日志有关的字典配置
logging.config.dictConfig(LOGGING_CONFIG)
# logging.getLogger('可指定任意的日志名')
# 查找规则:
# 1> 根据日志名会去字典的loggers属性里面找,没找到就用root根logger的配置!
# 2> 不传值,也用root根logger的配置!
# 冒泡规则:
# 若'propagate': True ,那么当消息的级别符合当前logger的级别后<这是前提>,才会同时会触发root根logger的配置!!
# 冒泡触发的root根logger的配置不用管其级别和过滤器(官网上是这样写的!!)
"""测试1
cmd终端无打印,demo.log文件中无写入
"""
# logger_object = logging.getLogger("nb")
# logger_object.debug('66666667662')
""" 测试2
cmd终端打印
2024-02-19 20:47:29 PM INFO MainThread : 66666667662 # 名为"nb"的logger导致的
2024-02-19 20:47:29 PM INFO MainThread : 66666667662 # 名为"nb"的logger成功后,冒泡了下,root根logger导致的
demo.log文件中写入
2024-02-19 20:47:29 PM INFO MainThread : 66666667662 # 名为"nb"的logger导致的
"""
# logger_object = logging.getLogger("nb")
# logger_object.info('66666667662')
""" 测试3
cmd终端无打印,demo.log文件中无写入
"""
# logger_object = logging.getLogger("why")
# logger_object.info('77777777777')
""" 测试3
cmd终端打印
2024-02-19 21:24:48 PM ERROR MainThread : 77777777777
demo.log文件中没有内容写入
"""
logger_object = logging.getLogger("why")
logger_object.info('77777777777')
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 示例2: 多文件配置
无非就是logger多绑定了几个 文件相关的handler
相较于示例1, 多的知识点就在于 该示例中 有按照文件大小分割的日志文件配置和按照时间日期分割的日志文件配置!!
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": True,
'formatters': {
'standard': {
'format': '{asctime} {levelname} {threadName} : {message}',
'style': '{',
"datefmt": '%Y-%m-%d %H:%M:%S %p',
},
'simple': {
'format': '%(asctime)s %(levelname)s %(message)s',
'style': '%',
"datefmt": '%Y-%m-%d',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
'error': {
# 错误日志 -- 按照文件大小分割
"class": 'logging.handlers.RotatingFileHandler',
'formatter': 'standard',
'filename': 'error.log',
'maxBytes': 1024 * 1025 * 50, # 根据文件大小拆分日志 50M
'backupCount': 5,
"encoding": "utf-8"
},
'run': {
# ψ(`∇´)ψ 运行日志 -- 按天自动分割
# filename、when、interval、backupCount、encoding都是TimedRotatingFileHandler类的参数!!!
"class": 'logging.handlers.TimedRotatingFileHandler',
'formatter': 'standard',
'filename': "run.log",
'when': 'D', # 根据时间纬度拆分日志 'D'天、'H'时、'M'分、'S'秒、'W{0-6}'周
'interval': 1, # 多少个时间纬度进行分割, 此示例中是 1天
'backupCount': 3, # 保留历史的几份日志
"encoding": "utf-8"
},
},
'loggers': {
'run': {
'handlers': ['run'],
'level': "INFO", # >=20 则触发日志
'propagate': True # ★ 表明是否触发父级日志?!
},
'error': {
'handlers': ['console', 'error'],
'level': "ERROR", # >=40 则触发日志
'propagate': False
}
},
# 根logger的配置,就是所谓的父级日志.
'root': {
'handlers': ['console', ],
'level': 'DEBUG',
'propagate': True
}
}
if __name__ == '__main__':
logging.config.dictConfig(LOGGING_CONFIG)
# root = logging.getLogger() # 没传值,或传的值没找到对应的logger名,都用的是 根logger
# root.info("测试测试")
run = logging.getLogger('run')
run.info("测试测试1") # 消息级别满足名为run的logger的配置,所以冒泡生效,根logger也会执行了
run.debug("测试测试1") # 消息级别不满足名为run的logger的配置,所以冒泡不生效,根logger也不会执行了
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 示例3: 过滤
在输出到指定目标(终端/文件)之前, 做一点筛选条件, 不满足就不能成功输出过去!!
过滤的配置文件
import logging
class DynamicFilter(logging.Filter):
def filter(self, record):
"""此处的筛选条件: 日志信息不为空 """
# record, 包含了日志的所有信息
# 可通过from logging import LogRecord 查看相关源码进行了解 eg:record.msg就是日志信息
# print(record, type(record))
if not record.msg:
return False
return True
class CallbackFilter(logging.Filter):
def __init__(self, callback):
# "callback": lambda record: len(record.msg) > 4
self.callback = callback
def filter(self, record):
"""此处的筛选条件: 日志信息的长度得大于4个字节 """
if self.callback(record):
return True
return False
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
日志配置文件
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": True,
'formatters': {
'standard': {
'format': '{asctime} {levelname} {threadName} :{message}',
'style': '{',
"datefmt": '%Y-%m-%d %H:%M:%S %p',
},
'simple': {
'format': '%(asctime)s %(levelname)s %(message)s',
'style': '%',
"datefmt": '%Y-%m-%d',
},
},
"filters": {
"dy": {
"()": "utils.DynamicFilter"
},
"call": {
"()": "utils.CallbackFilter",
# ★★★ 当CallbackFilter类初始化时,会将 callback的值/匿名函数 传递过去
"callback": lambda record: len(record.msg) > 4
},
"DebugFalse": {
"()": "django.utils.log.RequireDebugFalse" # 可看下RequireDebugFalse的源码就明白了!! 线上环境
},
"DebugTrue": {
"()": "django.utils.log.DebugTrue" # 开发环境
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
'run': {
# 运行日志,按天自动分割
"class": 'logging.handlers.TimedRotatingFileHandler',
'formatter': 'standard',
'filters': ["dy", 'call'], # ★★★ 同时都满足才可以哦!!
'filename': "run.log",
'when': 'D',
'interval': 1,
'backupCount': 3,
"encoding": "utf-8"
},
'error': {
# 错误日志,按照文件大小分割
"class": 'logging.handlers.RotatingFileHandler',
'formatter': 'standard',
'filename': 'error.log',
'maxBytes': 1024 * 1025 * 50,
'backupCount': 5,
"encoding": "utf-8"
},
},
'loggers': {
'run': {
'handlers': ['run'],
'level': "INFO", # >=20 则触发日志
'propagate': True
},
'error': {
'handlers': ['console', 'error'],
'level': "ERROR", # >=40 则触发日志
'propagate': False
}
},
'root': {
'handlers': ['console', ],
'level': 'DEBUG',
'propagate': True
}
}
if __name__ == '__main__':
logging.config.dictConfig(LOGGING_CONFIG)
run = logging.getLogger('run')
run.info("测试Hello")
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# Django中使用日志
对Django使用 logging日志配置!!
Step1: 将下方代码使用cv大法添加到Django的settings.py配置文件中!!
###########
# LOGGING #
# from django.conf import global_settings
###########
import os
# 启动Django后,会自动在项目根目录下创建log文件夹,并根据handler的配置自动生成error.log和run.log文件
BASE_LOG_DIR = BASE_DIR / 'log'
BASE_LOG_DIR.mkdir(exist_ok=True) # 不存在就创建,存在则不管啦
"""等同于
BASE_LOG_DIR = os.path.join(BASE_DIR, "log")
# os.makedirs(BASE_LOG_DIR,exist_ok=True)
if not os.path.exists(BASE_LOG_DIR):
os.makedirs(BASE_LOG_DIR)
"""
LOGGING_CONFIG = 'logging.config.dictConfig'
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
'formatters': {
'standard': {
'format': '{asctime} {levelname} {threadName} :{message}',
'style': '{',
"datefmt": '%Y-%m-%d %H:%M:%S %p',
},
'simple': {
'format': '%(asctime)s %(levelname)s %(message)s',
'style': '%',
"datefmt": '%Y-%m-%d',
},
},
# "filters": {
# "DebugFalse": {
# "()": "django.utils.log.RequireDebugFalse" # 可看下RequireDebugFalse的源码就明白了!! 线上环境
# },
# "DebugTrue": {
# "()": "django.utils.log.DebugTrue" # 开发环境
# },
# "call": {
# "()": "django.utils.log.CallbackFilter",
# "callback": lambda record: len(record.msg) > 4
# }
# },
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
# 'filters': ["DebugTrue"],
},
'run': {
"class": 'logging.handlers.TimedRotatingFileHandler',
'formatter': 'standard',
# 'filters': ["DebugFalse", 'call'],
'filename': os.path.join(BASE_LOG_DIR, 'run.log'),
'when': 'D',
'interval': 1,
'backupCount': 3,
"encoding": "utf-8"
},
'error': {
"class": 'logging.handlers.RotatingFileHandler',
'formatter': 'standard',
'filename': os.path.join(BASE_LOG_DIR, 'error.log'),
'maxBytes': 1024 * 1025 * 50,
'backupCount': 5,
"encoding": "utf-8"
},
},
'loggers': {
'run': {
'handlers': ['run'],
'level': "INFO",
'propagate': True
},
'error': {
'handlers': ['console', 'error'],
'level': "ERROR",
'propagate': False
}
},
'root': {
'handlers': ['console', ],
'level': 'DEBUG',
'propagate': True
}
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
Step2: 在utils文件夹下创建log.py文件. 写入以下代码.
import logging
# logger = logging.getLogger('run') # -- 生成一个logging对象!!
# -- 通常定义成一个方法来使用
def get_logger(l="run"):
return logging.getLogger(l)
# 注:若日志的记录导入这个模块时都不传参,使用默认参数.那就是单例啦.因为返回的都是同一对象嘛!
2
3
4
5
6
7
8
Step3: 导入使用
from utils import log
logger = log.get_logger() # 视图文件全局中
logger.info('Hello World') # 视图函数中
""" 其实本质就是这样的
import logging
logger = logging.getLogger("run")
logger.error("Hello World")
""
2
3
4
5
6
7
8
9