專案開發階段,顯示足夠的除錯資訊以輔助開發人員除錯程式碼還是非常必要的;專案上線以後,將系統執行時出現的警告、錯誤等資訊記錄下來以備相關人員瞭解系統執行狀況並維護程式碼也是很有必要的。與此同時,採集日誌資料也是為網站做數字化運營奠定一個基礎,透過對系統執行日誌的分析,我們可以監測網站的流量以及流量分佈,同時還可以挖掘出使用者的使用習慣和行為模式。
接下來,我們先看看如何透過Django的配置檔案來配置日誌。Django的日誌配置基本可以參照官方文件再結合專案實際需求來進行,這些內容基本上可以從官方文件上覆制下來,然後進行區域性的調整即可,下面給出一些參考配置。
LOGGING = {
'version': 1,
# 是否禁用已經存在的日誌器
'disable_existing_loggers': False,
# 日誌格式化器
'formatters': {
'simple': {
'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
'verbose': {
'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] '
'%(module)s.%(funcName)s line %(lineno)d: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
}
},
# 日誌過濾器
'filters': {
# 只有在Django配置檔案中DEBUG值為True時才起作用
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 日誌處理器
'handlers': {
# 輸出到控制檯
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'filters': ['require_debug_true'],
'formatter': 'simple',
},
# 輸出到檔案(每週切割一次)
'file1': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'access.log',
'when': 'W0',
'backupCount': 12,
'formatter': 'simple',
'level': 'INFO',
},
# 輸出到檔案(每天切割一次)
'file2': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'error.log',
'when': 'D',
'backupCount': 31,
'formatter': 'verbose',
'level': 'WARNING',
},
},
# 日誌器記錄器
'loggers': {
'django': {
# 需要使用的日誌處理器
'handlers': ['console', 'file1', 'file2'],
# 是否向上傳播日誌資訊
'propagate': True,
# 日誌級別(不一定是最終的日誌級別)
'level': 'DEBUG',
},
}
}
大家可能已經注意到了,上面日誌配置中的formatters
是日誌格式化器,它代表瞭如何格式化輸出日誌,其中格式佔位符分別表示:
%(name)s
- 記錄器的名稱%(levelno)s
- 數字形式的日誌記錄級別%(levelname)s
- 日誌記錄級別的文字名稱%(filename)s
- 執行日誌記錄呼叫的原始檔的檔名稱%(pathname)s
- 執行日誌記錄呼叫的原始檔的路徑名稱%(funcName)s
- 執行日誌記錄呼叫的函式名稱%(module)s
- 執行日誌記錄呼叫的模組名稱%(lineno)s
- 執行日誌記錄呼叫的行號%(created)s
- 執行日誌記錄的時間%(asctime)s
- 日期和時間%(msecs)s
- 毫秒部分%(thread)d
- 執行緒ID(整數)%(threadName)s
- 執行緒名稱%(process)d
- 程序ID (整數)
日誌配置中的handlers用來指定日誌處理器,簡單的說就是指定將日誌輸出到控制檯還是檔案又或者是網路上的伺服器,可用的處理器包括:
logging.StreamHandler(stream=None)
- 可以向類似與sys.stdout
或者sys.stderr
的任何檔案物件輸出資訊logging.FileHandler(filename, mode='a', encoding=None, delay=False)
- 將日誌訊息寫入檔案logging.handlers.DatagramHandler(host, port)
- 使用UDP協議,將日誌資訊傳送到指定主機和埠的網路主機上logging.handlers.HTTPHandler(host, url)
- 使用HTTP的GET或POST方法將日誌訊息上傳到一臺HTTP 伺服器logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)
- 將日誌訊息寫入檔案,如果檔案的大小超出maxBytes
指定的值,那麼將重新生成一個檔案來記錄日誌logging.handlers.SocketHandler(host, port)
- 使用TCP協議,將日誌資訊傳送到指定主機和埠的網路主機上logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=1.0)
- 將日誌輸出到指定的郵件地址logging.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True)
- 將日誌輸出到記憶體指定的緩衝區中
上面每個日誌處理器都指定了一個名為level
的屬性,它代表了日誌的級別,不同的日誌級別反映出日誌中記錄資訊的嚴重性。Python中定義了六個級別的日誌,按照從低到高的順序依次是:NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。
最後配置的日誌記錄器是用來真正輸出日誌的,Django框架提供瞭如下所示的內建記錄器:
django
- 在Django層次結構中的所有訊息記錄器django.request
- 與請求處理相關的日誌訊息。5xx響應被視為錯誤訊息;4xx響應被視為為警告訊息django.server
- 與透過runserver呼叫的伺服器所接收的請求相關的日誌訊息。5xx響應被視為錯誤訊息;4xx響應被記錄為警告訊息;其他一切都被記錄為INFOdjango.template
- 與模板渲染相關的日誌訊息django.db.backends
- 有與資料庫互動產生的日誌訊息,如果希望顯示ORM框架執行的SQL語句,就可以使用該日誌記錄器。
日誌記錄器中配置的日誌級別有可能不是最終的日誌級別,因為還要參考日誌處理器中配置的日誌級別,取二者中級別較高者作為最終的日誌級別。
如果想除錯你的Django專案,你一定不能不過名為Django-Debug-Toolbar的神器,它是專案開發階段輔助除錯和最佳化的必備工具,只要配置了它,就可以很方便的檢視到如下表所示的專案執行資訊,這些資訊對除錯專案和最佳化Web應用效能都是至關重要的。
專案 | 說明 |
---|---|
Versions | Django的版本 |
Time | 顯示檢視耗費的時間 |
Settings | 配置檔案中設定的值 |
Headers | HTTP請求頭和響應頭的資訊 |
Request | 和請求相關的各種變數及其資訊 |
StaticFiles | 靜態檔案載入情況 |
Templates | 模板的相關資訊 |
Cache | 快取的使用情況 |
Signals | Django內建的訊號資訊 |
Logging | 被記錄的日誌資訊 |
SQL | 向資料庫傳送的SQL語句及其執行時間 |
-
安裝Django-Debug-Toolbar。
pip install django-debug-toolbar
-
配置 - 修改settings.py。
INSTALLED_APPS = [ 'debug_toolbar', ] MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ] DEBUG_TOOLBAR_CONFIG = { # 引入jQuery庫 'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js', # 工具欄是否摺疊 'SHOW_COLLAPSED': True, # 是否顯示工具欄 'SHOW_TOOLBAR_CALLBACK': lambda x: True, }
-
配置 - 修改urls.py。
if settings.DEBUG: import debug_toolbar urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls)))
-
在配置好Django-Debug-Toolbar之後,頁面右側會看到一個除錯工具欄,如下圖所示,上面包括瞭如前所述的各種除錯資訊,包括執行時間、專案設定、請求頭、SQL、靜態資源、模板、快取、訊號等,檢視起來非常的方便。
在配置了日誌或Django-Debug-Toolbar之後,我們可以檢視一下之前將老師資料匯出成Excel報表的檢視函式執行情況,這裡我們關注的是ORM框架生成的SQL查詢到底是什麼樣子的,相信這裡的結果會讓你感到有一些意外。執行Teacher.objects.all()
之後我們可以注意到,在控制檯看到的或者透過Django-Debug-Toolbar輸出的SQL是下面這樣的:
SELECT `tb_teacher`.`no`, `tb_teacher`.`name`, `tb_teacher`.`detail`, `tb_teacher`.`photo`, `tb_teacher`.`good_count`, `tb_teacher`.`bad_count`, `tb_teacher`.`sno` FROM `tb_teacher`; args=()
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,)
這裡的問題通常被稱為“1+N查詢”(有的地方也將其稱之為“N+1查詢”),原本獲取老師的資料只需要一條SQL,但是由於老師關聯了學科,當我們查詢到N
條老師的資料時,Django的ORM框架又向資料庫發出了N
條SQL去查詢老師所屬學科的資訊。每條SQL執行都會有較大的開銷而且會給資料庫伺服器帶來壓力,如果能夠在一條SQL中完成老師和學科的查詢肯定是更好的做法,這一點也很容易做到,相信大家已經想到怎麼做了。是的,我們可以使用連線查詢,但是在使用Django的ORM框架時如何做到這一點呢?對於多對一關聯(如投票應用中的老師和學科),我們可以使用QuerySet
的用select_related()
方法來載入關聯物件;而對於多對多關聯(如電商網站中的訂單和商品),我們可以使用prefetch_related()
方法來載入關聯物件。
在匯出老師Excel報表的檢視函式中,我們可以按照下面的方式最佳化程式碼。
queryset = Teacher.objects.all().select_related('subject')
事實上,用ECharts生成前端報表的檢視函式中,查詢老師好評和差評資料的操作也能夠最佳化,因為在這個例子中,我們只需要獲取老師的姓名、好評數和差評數這三項資料,但是在預設的情況生成的SQL會查詢老師表的所有欄位。可以用QuerySet
的only()
方法來指定需要查詢的屬性,也可以用QuerySet
的defer()
方法來指定暫時不需要查詢的屬性,這樣生成的SQL會透過投影操作來指定需要查詢的列,從而改善查詢效能,程式碼如下所示:
queryset = Teacher.objects.all().only('name', 'good_count', 'bad_count')
當然,如果要統計出每個學科的老師好評和差評的平均數,利用Django的ORM框架也能夠做到,程式碼如下所示:
queryset = Teacher.objects.values('subject').annotate(good=Avg('good_count'), bad=Avg('bad_count'))
這裡獲得的QuerySet
中的元素是字典物件,每個字典中有三組鍵值對,分別是代表學科編號的subject
、代表好評數的good
和代表差評數的bad
。如果想要獲得學科的名稱而不是編號,可以按照如下所示的方式調整程式碼:
queryset = Teacher.objects.values('subject__name').annotate(good=Avg('good_count'), bad=Avg('bad_count'))
可見,Django的ORM框架允許我們用面向物件的方式完成關係資料庫中的分組和聚合查詢。