Django搭建個人博客:日誌記錄

上一章學習了自動化測試,很好,現在我們可以絞盡腦汁寫出一份全面的測試,來保證代碼永遠健康了。

話雖如此,但是作爲一個獨立開發者很難寫出真正全面的測試代碼。這是因爲用戶在使用你的網站時可不會循規蹈矩,而是會以各種怪異的姿勢瀏覽網頁、上傳數據。但這也不是壞事,用戶就是天然的測試人員,他們會很可愛的幫你找出一大堆的bug,陪你度過難眠的夜晚(伴隨着編程能力的提升)。

現在的問題是,開發者如何得知用戶到底遇到了哪些問題?用戶們大部分都與你素昧平生,分部在世界各地。更糟糕的是,部署在線上時由於配置了DEBUG = False,出錯時並不會出現報錯頁面,連用戶自己都不清楚到底是哪裏有bug。

Django給你的答案:日誌

日誌的組成

日誌是指程序在運行過程中,對狀態、時間、錯誤等的記錄。即把運行過程中產生的信息輸出或保存起來,供開發者查閱。

Django使用Python內置的logging模塊處理日誌。關於該模塊的使用,Python文檔裏有非常詳細的討論。如果你從未用過,本文提供一個快速入門。

日誌事件的信息流程如下:

[外鏈圖片轉存失敗(img-4hXz79Wf-1563378023854)(https://www.dusaiphoto.com/media/image/image_source/20190716/logging_flow.jpg)]

這個圖看不懂也沒關係。以後你需要深度使用日誌時,會回來仔細研究它的。

一份日誌配置由LoggersHandlersFiltersFormatters四部分組成。

Loggers

Logger記錄器,是日誌系統的入口。它有三個重要的工作

  • 嚮應用程序(也就是你的項目)公開幾種方法,以便運行時記錄消息
  • 根據傳遞給Logger的消息的嚴重性,確定出需要處理的消息
  • 將需要處理的消息傳遞給所有感興趣的處理器(Handler

每一條寫入logger的消息都是一條日誌記錄。每一條日誌記錄也包含級別,代表對應消息的嚴重程度。常用的級別如下:

  • DEBUG:排查故障時使用的低級別系統信息,通常開發時使用
  • INFO:一般的系統信息,並不算問題
  • WARNING:描述系統發生的小問題的信息,但通常不影響功能
  • ERROR:描述系統發生的大問題的信息,可能會導致功能不正常
  • CRITICAL:描述系統發生嚴重問題的信息,應用程序有崩潰風險

當logger處理一條消息時,會將自己的日誌級別和這條消息的日誌級別做對比。如果消息的級別匹配或者高於logger的日誌級別,它就會被進一步處理;否則這條消息就會被忽略掉。

當logger確定了一條消息需要處理之後,會把它傳給Handler

Handlers

Handler處理器,它的主要功能是決定如何處理logger中每一條消息,比如把消息輸出到屏幕、文件或者Email中。

和logger一樣,handler也有級別的概念。如果一條日誌記錄的級別不匹配或者低於handler的日誌級別,則會被handler忽略。

一個logger可以有多個handler,每一個handler可以有不同的日誌級別。這樣就可以根據消息的重要性不同,來提供不同類型的輸出。例如,你可以添加一個handler把ERRORCRITICAL消息發到你的Email,再添加另一個 handler把所有的消息(包括ERRORCRITICAL消息)保存到文件裏。

Filters

Filter過濾器。在日誌記錄從logger傳到handler的過程中,使用Filter做額外的控制。例如只允許某個特定來源的ERROR消息輸出。

Filter還被用來在日誌輸出之前對日誌記錄做修改。例如當滿足一定條件時,把日誌記錄從 ERROR 降到 WARNING 級別。

Filter在logger和handler中都可以添加;多個filter可以鏈接起來使用,來做多重過濾操作。

Formatters

Formatter即格式化器,主要功能是確定最終輸出的形式和內容

日誌配置示例

說了這麼多腦殼都說暈了,接下來看兩個示例。

簡單示例

在Django中可以通過字典的形式對整個項目的日誌進行配置,配置的位置當然是在settings.py中了。一個簡單的配置如下:

my_blog/settings.py

...
import os

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'logs/debug.log'),
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

字典中的version指明瞭配置的版本;disable_existing_loggers指明是否禁止默認配置的記錄器。這兩項通常不需要去改動,重點看下loggershandlers的配置:

  • 如前面說,一條消息首先傳遞給logger。Django中內置了幾種記錄器,比如這裏用到的Django記錄器,它會接收Django層次結構中的所有消息。然後我們定義了需要處理DEBUG以上級別的消息,並把這些消息傳遞給名叫file的處理器。'propagate': True意思是本記錄器處理過的消息其他處理器也可以繼續處理。
  • 現在消息來到名叫filehandlers中了。這個處理器定義了消息處理級別仍然爲DEBUG,在class中定義將消息輸出到文件中去,文件地址爲項目目錄的logs/debug.log
  • 因爲這裏沒有配置filtersformatters,因此會採用默認的設置。

需要注意的是日誌的輸出文件的目錄logs/一定要提前創建好,並且確保項目擁有此目錄的寫入權限。

這個日誌系統就配置好了!接下來運行項目,隨便刷新幾個頁面看看debug.log中有沒有寫入消息:

logs/debug.log

(0.001) 
            SELECT name, type FROM sqlite_master
            WHERE type in ('table', 'view') AND NOT name='sqlite_sequence'
            ORDER BY name; args=None
(0.000) SELECT "django_migrations"."app", "django_migrations"."name" FROM "django_migrations"; args=()
...
...
...

debug.log文件中出現了一大堆冗長的信息,因爲DEBUG級別會包含所有的數據庫查詢記錄。

默認情況下,僅在調試模式下才會顯示DEBUG級別的消息日誌,部署在線上時只會將INFO或以上的信息進行記錄。

再試試別的。把上面代碼中記錄器和處理器的日誌級別都改爲INFO

LOGGING = {
    ...
    'handlers': {
        'file': {
            'level': 'INFO',
            ...
        },
    },
    'loggers': {
        'django': {
            'level': 'INFO',
            ...
        },
    },
}

再刷新幾次界面,看看輸出的內容:

"GET /article/article-list/ HTTP/1.1" 200 14438
"GET /article/article-detail/32/ HTTP/1.1" 200 33364
"GET /accounts/login/ HTTP/1.1" 200 7180
...

這次清爽多了,輸出的主要是頁面的拉取信息。

讓我們再看看ERROR信息長什麼樣的。在地址欄輸入一個不存在的文章詳情頁面地址:

http://127.0.0.1:8000/article/article-detail/9999/

很明顯這會得到一個數據不存在的報錯:

Internal Server Error: /article/article-detail/9999/
Traceback (most recent call last):
  File "E:\django_project\env\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
    response = get_response(request)
    ...
article.models.ArticlePost.DoesNotExist: ArticlePost matching query does not exist.
"GET /article/article-detail/9999/ HTTP/1.1" 500 80792

ERROR日誌輸出了整個bug的回溯,和你在瀏覽器中的報錯是完全一樣的,這些信息就非常的有用了。基本上ERROR信息能夠暴露出用戶在使用你的網站過程中的大部分問題;也就是說每一個ERROR都是需要你去解決掉的。ERROR信息的錯誤碼通常都是“500”,也就是服務器內部錯誤的代碼。

不過仔細想想,似乎找不到對應的資源在很多時候並不是bug,而是用戶在輸入url時自己犯了錯誤。所以我們把文章詳情視圖的ArticlePost.objects.get(id=id)改成get_object_or_404(ArticlePost, id=id)試試:

article/views.py

...
from django.shortcuts import get_object_or_404

def article_detail(request, id):
    # 取出相應的文章
    # article = ArticlePost.objects.get(id=id)
    article = get_object_or_404(ArticlePost, id=id)

    ...

服務器重啓後再次刷新一個不存在的頁面,看看日誌:

Not Found: /article/article-detail/9999/
"GET /article/article-detail/9999/ HTTP/1.1" 404 1780

現在它不是一條ERROR信息了,而是變爲了WARNING,所以也沒有了錯誤回溯(錯誤碼也由 500 變成了 404)。這裏就能看出這兩個方法的重要區別了;在項目中到底選擇哪個沒有定論,還是以你的具體需求決定。

複雜示例

接下來再看一個複雜的日誌配置:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'filters': {
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'formatter': 'verbose',
        },
        'file': {
            'level': 'WARNING',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'logs/debug.log'),
            'formatter': 'verbose',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'propagate': True,
        },
        'django.request': {
            'handlers': ['file', 'mail_admins'],
            'level': 'WARNING',
            'propagate': False,
        },
    }
}

讓我們來分解一下此配置。

配置中定義了兩個格式化器

  • verbose:詳細的格式化器,依次輸出:消息級別、發生時間、拋出模塊、進程ID、線程ID、提示信息
  • simple:簡要的格式化器,僅輸出消息級別和提示信息

一個過濾器

  • require_debug_true:使用此過濾器的消息僅在調試時纔會生效

三個處理器

  • console:處理INFO以上級別消息,輸出簡要信息到命令行中;此處理器僅在調試模式生效
  • mail_admins:處理ERROR以上級別消息,輸出詳細信息到Email中
  • file:處理WARNING以上級別消息,輸出詳細信息到文件中

兩個記錄器

  • django:將django產生的所有消息轉交給console處理器
  • django.request:將網絡請求相關消息轉交給filemail_admins這兩個處理器。注意這裏的'propagate': False使得此記錄器處理過的消息就不再讓django記錄器再次處理了

讀者可以嘗試製造不同級別的消息,看看日誌系統是否正常工作。當然最重要的,跟Email有關的配置一定要事先把Email給設置好,即下面的內容填成你的:

# SMTP服務器
EMAIL_HOST = 'your smtp'
# 郵箱名
EMAIL_HOST_USER = 'your email'
# 郵箱密碼
EMAIL_HOST_PASSWORD = 'your password'
# 發送郵件的端口
EMAIL_PORT = 25
# 是否使用 TLS
EMAIL_USE_TLS = True
# 默認的發件人
DEFAULT_FROM_EMAIL = 'your email'

日誌分割

現在我們已經可以愉快的記錄日誌了,接下來一個問題是如何去分割日誌?假設你的網站能夠有幸運行十年時間,如果不間斷的往同一個文件中寫日誌信息,最終它會變成一個拖垮服務器的龐然大物。

最好是日誌能夠按自然天進行記錄和分割。好在這個問題也不需要你去費腦筋,Python幫你搞定了。

只需要把處理器稍稍改一下:

my_blog/settings.py

...
LOGGING = {
    ...
    'handlers': {
        ...
        'file': {
            ...
            # 註釋掉 class
            # 'class': 'logging.FileHandler',
            
            #新增內容
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'when': 'midnight',
            'backupCount': 30,
            
        },
    },
    ...
}
  • TimedRotatingFileHandler:Python內置的隨時間分割日誌文件的模塊
  • when:分割時間爲凌晨
  • backupCount:日誌文件保存日期爲30天

接下來把系統時間往後調一天,然後重新啓動服務器:

python manage.py runserver --noreload

注意這次啓動有點不一樣,後面有個--noreload後綴。這是因爲通常Django的調試服務器運行時會順帶啓動重載器,所以每當重載器檢測到代碼有變化後,會自動重啓服務器,相當的方便。但問題是分割文件與重載器同時操作日誌文件會產生衝突,因此這裏一定要用--noreload暫時將重載器禁止掉。

然後就可以愉快的刷幾條消息到文件中啦。你會發現老日誌已經更名爲debug.log.2019-07-17了,而剛刷的新消息則保存在debug.log中。

除了上面介紹的TimedRotatingFileHandler,Python還提供了一個按照文件大小分割的RotatingFileHandler。有興趣的看Python官方文檔

自定義日誌

內置配置實際上已經能夠滿足90%以上的日誌需求了,但總有時候你想在一些奇怪的地方進行記錄,這就需要你自己在代碼中插入自定義的日誌記錄代碼了。

自定義日誌用起來也是相當方便的:

from my_blog.settings import LOGGING
import logging

logging.config.dictConfig(LOGGING)
logger = logging.getLogger('django.request')

def whatever(request):
    # do something
    logger.warning('Something went wrong!')
    # do something else

導入剛纔寫的日誌框架並將django.request配置到logger對象中。然後你就可以在任何地方安插任何級別的消息了。消息內容可以用字符串的格式化方法(str.format()),玩出各種花樣。

關於日誌的入門介紹就到此爲止了,想深入學習的讀者請繼續閱讀本文的參考文章:

總結

和上章類似,本章的內容也是概念偏多,希望讀者儘可能去理解,最起碼要囫圇吞棗的把日誌成功移植到你的項目中去。獲取一份好的日誌,有時候遠比開發一個無關緊要的新功能更重要。

比較起來博主認爲對博客項目來說,日誌比測試還重要,畢竟用戶的使用體驗是最佳的實踐。

但請不要誤會我的意思。測試和日誌就像兩兄弟,測試解決開發中的問題,日誌解決維護中的問題。有機的結合起來,你的項目才能夠長期穩定健康。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章