技術博客:github.com/yongxinz/te…python
同時,也歡迎關注個人微信公衆號 AlwaysBeta,更多精彩內容等你來。 git
日誌是個好東西,但卻並非全部人都願意記,直到出了問題才追悔莫及,長嘆一聲,當初要是記日誌就行了。github
但記日誌倒是個技術活,不能什麼都不記,但也不能什麼都記。若是記了不少沒用的信息,反而給查日誌排錯的過程增長不少困難。web
因此,日誌要記錄在程序的關鍵節點,並且內容要簡潔,傳遞信息要準確。要清楚的反應出程序當時的狀態,時間,錯誤信息等。django
只有作到這樣,咱們才能在第一時間找到問題,而且解決問題。json
在 Django 中使用 Python 的標準庫 logging 模塊來記錄日誌,關於 logging 的配置,我這裏不作過多介紹,只寫其中最重要的四個部分:Loggers
、Handlers
、Filters
和 Formatters
。api
Logger
即記錄器,是日誌系統的入口。它有三個重要的工做:微信
Logger
的消息的嚴重性,肯定消息是否須要處理Handler
每一條寫入 Logger
的消息都是一條日誌記錄,每一條日誌記錄都包含級別,表明對應消息的嚴重程度。經常使用的級別以下:restful
DEBUG
:排查故障時使用的低級別系統信息,一般開發時使用INFO
:通常的系統信息,並不算問題WARNING
:描述系統發生小問題的信息,但一般不影響功能ERROR
:描述系統發生大問題的信息,可能會致使功能不正常CRITICAL
:描述系統發生嚴重問題的信息,應用程序有崩潰的風險當 Logger
處理一條消息時,會將本身的日誌級別和這條消息配置的級別作對比。若是消息的級別匹配或者高於 Logger
的日誌級別,它就會被進一步處理,不然這條消息就會被忽略掉。cookie
當 Logger
肯定了一條消息須要處理以後,會把它傳給 Handler
。
Handler
即處理器,它的主要功能是決定如何處理 Logger
中的每一條消息,好比把消息輸出到屏幕、文件或者 Email 中。
和 Logger
同樣,Handler
也有級別的概念。若是一條日誌記錄的級別不匹配或者低於 Handler
的日誌級別,則會被 Handler
忽略。
一個 Logger
能夠有多個 Handler
,每個 Handler
能夠有不一樣的日誌級別。這樣就能夠根據消息的重要性不一樣,來提供不一樣類型的輸出。例如,你能夠添加一個 Handler
把 ERROR
和 CRITICAL
消息發到你的 Email,再添加另外一個 Handler
把全部的消息(包括 ERROR
和 CRITICAL
消息)保存到文件裏。
Filter
即過濾器。在日誌記錄從 Logger
傳到 Handler
的過程當中,使用 Filter
來作額外的控制。例如,只容許某個特定來源的 ERROR
消息輸出。
Filter
還被用來在日誌輸出以前對日誌記錄作修改。例如,當知足必定條件時,把日誌級別從 ERROR
降到 WARNING
。
Filter
在 Logger
和 Handler
中均可以添加,多個 Filter
能夠連接起來使用,來作多重過濾操做。
Formatter
即格式化器,主要功能是肯定最終輸出的形式和內容。
說了這麼多理論,是時候來看看具體怎麼實現了。
其實最簡單的方式就是直接在文件開頭 import
,而後程序中調用,像下面這樣:
# import the logging library
import logging
# Get an instance of a logger
logging.basicConfig(
format='%(asctime)s - %(pathname)s[%(lineno)d] - %(levelname)s: %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
def my_view(request, arg1, arg):
...
if bad_mojo:
# Log an error message
logger.error('Something went wrong!')
複製代碼
但這種方式並很差,若是在每一個文件開頭都這樣寫一遍,第一是麻煩,第二是若是哪天要改變輸出日誌格式,那每一個文件都要改一遍,還不累死。
很顯然,若是能封裝成一個類,用的時候調用這個類,修改的時候也只須要修改這一個地方,是否是就解決這個問題了呢?
下面來看看具體這個類怎麼封裝:
class CommonLog(object):
""" 日誌記錄 """
def __init__(self, logger, logname='web-log'):
self.logname = os.path.join(settings.LOGS_DIR, '%s' % logname)
self.logger = logger
self.logger.setLevel(logging.DEBUG)
self.logger.propagate = False
self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s', '%Y-%m-%d %H:%M:%S')
def __console(self, level, message):
# 建立一個FileHandler,用於寫到本地
fh = logging.handlers.TimedRotatingFileHandler(self.logname, when='MIDNIGHT', interval=1, encoding='utf-8')
# fh = logging.FileHandler(self.logname, 'a', encoding='utf-8')
fh.suffix = '%Y-%m-%d.log'
fh.setLevel(logging.DEBUG)
fh.setFormatter(self.formatter)
self.logger.addHandler(fh)
# 建立一個StreamHandler,用於輸出到控制檯
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(self.formatter)
self.logger.addHandler(ch)
if level == 'info':
self.logger.info(message)
elif level == 'debug':
self.logger.debug(message)
elif level == 'warning':
self.logger.warning(message)
elif level == 'error':
self.logger.error(message)
# 這兩行代碼是爲了不日誌輸出重複問題
self.logger.removeHandler(ch)
self.logger.removeHandler(fh)
# 關閉打開的文件
fh.close()
def debug(self, message):
self.__console('debug', message)
def info(self, message):
self.__console('info', message)
def warning(self, message):
self.__console('warning', message)
def error(self, message):
self.__console('error', message)
複製代碼
這是我在項目中還在用的一段代碼,生成的文件按天進行切分。
當時寫這段代碼,有個問題折騰了我好久,就是顯示代碼報錯行數的問題。當 formatter
配置 %(lineno)d
時,每次並非顯示實際的報錯行,而是顯示日誌類中的代碼行,但這樣顯示就失去意義了,因此也就沒有配置,用了 %(name)s
來展現實際的調用文件。
其實,若是隻是爲了排錯方便,記錄一些日誌,這個類基本能夠知足要求。但若是要記錄訪問系統的全部請求日誌,那就無能爲力了,由於不可能手動在每一個接口代碼加日誌,也不必。
這個時候,很天然就能想到 Django 中間件了。
中間件日誌代碼一共分三個部分,分別是:Filters
代碼,middleware
代碼,settings
配置,以下:
local = threading.local()
class RequestLogFilter(logging.Filter):
""" 日誌過濾器 """
def filter(self, record):
record.sip = getattr(local, 'sip', 'none')
record.dip = getattr(local, 'dip', 'none')
record.body = getattr(local, 'body', 'none')
record.path = getattr(local, 'path', 'none')
record.method = getattr(local, 'method', 'none')
record.username = getattr(local, 'username', 'none')
record.status_code = getattr(local, 'status_code', 'none')
record.reason_phrase = getattr(local, 'reason_phrase', 'none')
return True
class RequestLogMiddleware(MiddlewareMixin):
""" 將request的信息記錄在當前的請求線程上。 """
def __init__(self, get_response=None):
self.get_response = get_response
self.apiLogger = logging.getLogger('web.log')
def __call__(self, request):
try:
body = json.loads(request.body)
except Exception:
body = dict()
if request.method == 'GET':
body.update(dict(request.GET))
else:
body.update(dict(request.POST))
local.body = body
local.path = request.path
local.method = request.method
local.username = request.user
local.sip = request.META.get('REMOTE_ADDR', '')
local.dip = socket.gethostbyname(socket.gethostname())
response = self.get_response(request)
local.status_code = response.status_code
local.reason_phrase = response.reason_phrase
self.apiLogger.info('system-auto')
return response
複製代碼
settings.py
文件配置:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 自定義中間件添加在最後
'lib.log_middleware.RequestLogMiddleware'
]
LOGGING = {
# 版本
'version': 1,
# 是否禁止默認配置的記錄器
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '{"time": "%(asctime)s", "level": "%(levelname)s", "method": "%(method)s", "username": "%(username)s", "sip": "%(sip)s", "dip": "%(dip)s", "path": "%(path)s", "status_code": "%(status_code)s", "reason_phrase": "%(reason_phrase)s", "func": "%(module)s.%(funcName)s:%(lineno)d", "message": "%(message)s"}',
'datefmt': '%Y-%m-%d %H:%M:%S'
}
},
# 過濾器
'filters': {
'request_info': {'()': 'lib.log_middleware.RequestLogFilter'},
},
'handlers': {
# 標準輸出
'console': {
'level': 'ERROR',
'class': 'logging.StreamHandler',
'formatter': 'standard'
},
# 自定義 handlers,輸出到文件
'restful_api': {
'level': 'DEBUG',
# 時間滾動切分
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOGS_DIR, 'web-log.log'),
'formatter': 'standard',
# 調用過濾器
'filters': ['request_info'],
# 天天凌晨切分
'when': 'MIDNIGHT',
# 保存 30 天
'backupCount': 30,
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'ERROR',
'propagate': False
},
'web.log': {
'handlers': ['restful_api'],
'level': 'INFO',
# 此記錄器處理過的消息就再也不讓 django 記錄器再次處理了
'propagate': False
},
}
}
複製代碼
經過這種方式,只要過 Django 的請求就都會有日誌,不論是 web 仍是 Django admin。具體記錄哪些字段能夠根據項目須要進行獲取和配置。
有一點須要注意的是,經過 request.user
來獲取用戶名只適用於 session
的認證方式,由於 session
認證以後會將用戶名賦值給 request.user
,因此才能取獲得。
假設用 jwt
方式認證,request.user
是沒有值的。想要獲取用戶名能夠有兩種方式:一是在日誌中間件中解析 jwt cookie
獲取用戶名,但這種方式並很差,更好的方法是重寫 jwt
認證,將用戶名賦值給 request.user
,這樣就能夠在其餘任何地方調用 request.user
來取值了。
以上就是在 Django 中記錄日誌的所有內容,但願你們都能好好記日誌,由於必定會用得上。
參考文檔:
docs.djangoproject.com/en/2.1/topi…