Django 中如何優雅的記錄日誌

技術博客:github.com/yongxinz/te…python

同時,也歡迎關注個人微信公衆號 AlwaysBeta,更多精彩內容等你來。 git

日誌是個好東西,但卻並非全部人都願意記,直到出了問題才追悔莫及,長嘆一聲,當初要是記日誌就行了。github

但記日誌倒是個技術活,不能什麼都不記,但也不能什麼都記。若是記了不少沒用的信息,反而給查日誌排錯的過程增長不少困難。web

因此,日誌要記錄在程序的關鍵節點,並且內容要簡潔,傳遞信息要準確。要清楚的反應出程序當時的狀態,時間,錯誤信息等。django

只有作到這樣,咱們才能在第一時間找到問題,而且解決問題。json

logging 結構

在 Django 中使用 Python 的標準庫 logging 模塊來記錄日誌,關於 logging 的配置,我這裏不作過多介紹,只寫其中最重要的四個部分:LoggersHandlersFiltersFormattersapi

Loggers

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

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

每一條寫入 Logger 的消息都是一條日誌記錄,每一條日誌記錄都包含級別,表明對應消息的嚴重程度。經常使用的級別以下:restful

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

Logger 處理一條消息時,會將本身的日誌級別和這條消息配置的級別作對比。若是消息的級別匹配或者高於 Logger 的日誌級別,它就會被進一步處理,不然這條消息就會被忽略掉。cookie

Logger 肯定了一條消息須要處理以後,會把它傳給 Handler

Handlers

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

Logger 同樣,Handler 也有級別的概念。若是一條日誌記錄的級別不匹配或者低於 Handler 的日誌級別,則會被 Handler 忽略。

一個 Logger 能夠有多個 Handler,每個 Handler 能夠有不一樣的日誌級別。這樣就能夠根據消息的重要性不一樣,來提供不一樣類型的輸出。例如,你能夠添加一個 HandlerERRORCRITICAL 消息發到你的 Email,再添加另外一個 Handler 把全部的消息(包括 ERRORCRITICAL 消息)保存到文件裏。

Filters

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

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

FilterLoggerHandler 中均可以添加,多個 Filter 能夠連接起來使用,來作多重過濾操做。

Formaters

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 中間件了。

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…

www.dusaiphoto.com/article/det…

juejin.im/post/5c3430…

www.xiaomastack.com/2019/01/11/…

相關文章
相關標籤/搜索