django配置多進程按日期分割處理日誌

一,Django日誌的基礎知識

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

1,Loggers

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

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

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

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

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

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

2,Handlers

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

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

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

3,Filters

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

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

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

4,Formaters

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

二,實現方式

1,使用django-logging模塊的class - RotatingFileHandler
1) 實現效果

設置RotatingFileHandler的maxBytes與backupCount,這兩個參數默認是0。 當兩個參數都不爲0時,會執行rallover過程:
log文件大小接近maxBytes時,新建一個文件作爲log的輸出,舊的文件會被加上類似’.1’、’.2’的後綴。 舉個例子,如果backupCount=5,log file定義的名字爲app.log,你會得到app.log, app.log.1, app.log.2 一直到 app.log.5。 然而被寫入日誌的永遠是app.log,寫滿了之後重命名爲app.log.1,如果app.log.1存在,app.log.1會先被重名名爲app.log.2,依此類推。 另外,如果app.log.5存在,它會被刪除。

2)實現代碼

在settings中配置以下代碼:

#LOGGING_DIR 日誌文件存放目錄
LOGGING_DIR = "logs" # 日誌存放路徑
if not os.path.exists(LOGGING_DIR):
    os.mkdir(LOGGING_DIR)
import logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': { #格式化器
        'standard': {
            'format': '[%(levelname)s][%(asctime)s][%(filename)s][%(funcName)s][%(lineno)d] > %(message)s'
            },
        'simple': {
            'format': '[%(levelname)s]> %(message)s'
            },
    },
    'filters': {
    'require_debug_true': {
        '()': 'django.utils.log.RequireDebugTrue',
        },
 },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'default': {                                            # 用於文件輸出
            'level': 'INFO',                                    # 輸出日誌等級
            'class': 'logging.handlers.RotatingFileHandler',    # 日誌類型
            'filename': '%s/django.log' % LOGGING_DIR,          # 具體日誌文件的名字
            'maxBytes': 1024 * 1024 * 2,                        # 日誌大小
            'backupCount': 1,                                   # 備份數量
            'formatter':'standard',                             # 輸出日誌格式
            'encoding': 'utf-8',                                # 輸出日誌編碼
        },
        'error': {
            'level': 'ERROR',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '%s/error.log' % LOGGING_DIR,
            'maxBytes': 1024 * 1024 * 2,
            'backupCount': 2,
            'formatter': 'standard',
            'encoding': 'utf-8',
        },
        'modify': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',  # 保存到文件,自動切
            'filename': '%s/modify.log' % LOGGING_DIR,
            'maxBytes': 1024 * 1024 * 5,  # 日誌大小 5M
            'backupCount': 4,
            'formatter': 'standard',
            'encoding': "utf-8"
        }
    },
    'loggers': {    #日誌分配到哪個handlers中
        'mydjango': {
            'handlers': ['console','default','error'],         # 上線之後可以把'console'移除
            'level':'DEBUG',
            'propagate': True,       # 向不向更高級別的logger傳遞
        },
        'modify': {  # 名爲 'modify'的logger還單獨處理
            'handlers': ['console', 'default', 'error', "modify"],
            'level': 'DEBUG',
            'propagate': False,
        },
 }
}
2,使用django-logging模塊的class - TimedRotatingFileHandler
1)實現代碼
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '[%(asctime)s] [%(levelname)s] %(message)s'
        },
    },
    'handlers': {
        # 輸出日誌的控制檯
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'verbose'
        },

        # 'default': {
        #     'level': 'INFO',
        #     'class': 'logging.handlers.TimedRotatingFileHandler',
        #     'filename':  '%s/django.log' % LOGGING_DIR,     # 日誌的文件名
        #     # TimedRotatingFileHandler的參數
        #     # 目前設定每天一個日誌文件
        #     # 'S'         |  秒
        #     # 'M'         |  分
        #     # 'H'         |  時
        #     # 'D'         |  天
        #     # 'W0'-'W6'   |  週一至週日
        #     # 'midnight'  |  每天的凌晨
        #     'when': 'midnight',                             # 間間隔的類型
        #     'interval': 1,                                  # 時間間隔
        #     'backupCount': 100,                             # 能留幾個日誌文件;過數量就會丟棄掉老的日誌文件
        #     'formatter': 'standard',                        # 日誌文本格式
        #     'encoding': 'utf-8',                            # 日誌文本編碼
        # },

        # 輸出日誌到文件,按日期滾動
        'file': {
            'level': 'DEBUG',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            # TimedRotatingFileHandler的參數
            # 參照https://docs.python.org/3/library/logging.handlers.html#timedrotatingfilehandler
            # 目前設定每天一個日誌文件
            'filename': 'logs/manage.log',
            'when': 'midnight',
            'interval': 1,
            'backupCount': 100,
            'formatter': 'verbose'
        },
        # 發送郵件,目前騰訊雲、阿里雲的服務器對外發送郵件都有限制,暫時不使用
        'email': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'include_html': True,
        }
    },
    'loggers': {
        # 不同的logger
        'django': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}
2)RotatingFileHandler 和 TimedRotatingFileHandler在多進程環境下會出現異常

異常如下:

  • 日誌寫入錯亂;
  • 日誌並沒有按天分割,而且還會丟失。

出現異常的原因爲:
Django logging 是基於 Python logging 模塊實現的,logging 模塊是線程安全的,但不能保證多進程安全。

以下爲對異常的詳細描述:

下面來詳細描述一下這個異常過程,假設我們每天生成一個日誌文件 error.log,每天凌晨進行日誌分割。那麼,在單進程環境下是這樣的:

生成 error.log 文件;
寫入一天的日誌;
零點時,判斷 error.log-2020-05-15 是否存在,如果存在則刪除;如果不存在,將 error.log 文件重命名爲 error.log-2020-05-15;
重新生成 error.log 文件,並將 logger 句柄指向新的 error.log。
再來看看多進程的情況:

生成 error.log 文件;
寫入一天的日誌;
零點時,1 號進程判斷 error.log-2020-05-15 是否存在,如果存在則刪除;如果不存在,將 error.log 文件重命名爲 error.log-2020-05-15;
此時,2 號進程可能還在向 error.log 文件進行寫入,由於寫入文件句柄已經打開,所以會向 error.log-2020-05-15 進行寫入;
2 號進程進行文件分割操作,由於 error.log-2020-05-15 已經存在,所以 2 號進程會將它刪除,然後再重命名 error.log,這樣就導致了日誌丟失;
由於 2 號進程將 error.log 重命名爲 error.log-2020-05-15,也會導致 1 號進程繼續向 error.log-2020-05-15 寫入,這樣就造成了寫入錯亂。
3,爲解決多進程問題,有如下兩種方案
1) ConcurrentLogHandler
pip install ConcurrentLogHandler
# settings.py
LOGGING = {
    ...
    'handlers': {
        ...
        'file': {
            'level': 'INFO',
            'class': 'cloghandler.ConcurrentRotatingFileHandler',
            'filename': os.path.join(LOGS_DIR, 'app.log'),
            'formatter': 'verbose',
            'maxBytes': 1024,
            'backupCount': 5
        },
        ...
    }
    ...
}

此包的最後更新時間是 2013.7,可見很久沒有人維護了。之後在這個官網又找到了這個包的升級版本concurrent-log-handler,即下面這個包

2)concurrent-log-handler
pip install concurrent-log-handler

在LOGGING中,用concurrent_log_handler.ConcurrentRotatingFileHandler代替logging.RotatingFileHandler

# settings.py
LOGGING = {
    ...
    'handlers': {
        ...
        'file': {
            'level': 'INFO',
            'class': 'concurrent_log_handler.ConcurrentRotatingFileHandler',
            'filename': os.path.join(LOGS_DIR, 'app.log'),
            'formatter': 'verbose',
            'maxBytes': 1024,
            'backupCount': 5
        },
        ...
    }
    ...
}

這個包目前還在有人維護。此包不足之處:
只有RotatingFileHandler的改動,也就是說,只對大小分割文件的handler 做了優化,並沒有對TimeRotatingFileHandler優化。這個包通過加鎖的方式實現了多進程安全,並且可以在日誌文件達到特定大小時,分割文件,但是不支持按時間分割。

三,自定義logger類

以上幾種方法均無法完全實現django配置多進程按日期分割處理日誌。我們考慮自定義實現。

1, 重寫 TimedRotatingFileHandler

通過上面的分析可以知道,出問題的點就是發生在日誌分割時,一是刪文件,二是沒有及時更新寫入句柄。
所以針對這兩點,我的對策就是:一是去掉刪文件的邏輯,二是在切割文件時,及時將寫入句柄更新到最新。

import os
import time
from logging.handlers import TimedRotatingFileHandler
class CommonTimedRotatingFileHandler(TimedRotatingFileHandler):
    @property
    def dfn(self):
        currentTime = int(time.time())
        # get the time that this sequence started at and make it a TimeTuple
        dstNow = time.localtime(currentTime)[-1]
        t = self.rolloverAt - self.interval
        if self.utc:
            timeTuple = time.gmtime(t)
        else:
            timeTuple = time.localtime(t)
            dstThen = timeTuple[-1]
            if dstNow != dstThen:
                if dstNow:
                    addend = 3600
                else:
                    addend = -3600
                timeTuple = time.localtime(t + addend)
        dfn = self.rotation_filename(self.baseFilename + "." + time.strftime(self.suffix, timeTuple))
        return dfn
    def shouldRollover(self, record):
        """
        是否應該執行日誌滾動操作:
        1、存檔文件已存在時,執行滾動操作
        2、當前時間 >= 滾動時間點時,執行滾動操作
        """
        dfn = self.dfn
        t = int(time.time())
        if t >= self.rolloverAt or os.path.exists(dfn):
            return 1
        return 0
    def doRollover(self):
        """
        執行滾動操作
        1、文件句柄更新
        2、存在文件處理
        3、備份數處理
        4、下次滾動時間點更新
        """
        if self.stream:
            self.stream.close()
            self.stream = None
        # get the time that this sequence started at and make it a TimeTuple
        dfn = self.dfn
        # 存檔log 已存在處理
        if not os.path.exists(dfn):
            self.rotate(self.baseFilename, dfn)
        # 備份數控制
        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                os.remove(s)
        # 延遲處理
        if not self.delay:
            self.stream = self._open()
        # 更新滾動時間點
        currentTime = int(time.time())
        newRolloverAt = self.computeRollover(currentTime)
        while newRolloverAt <= currentTime:
            newRolloverAt = newRolloverAt + self.interval
        # If DST changes and midnight or weekly rollover, adjust for this.
        if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
            dstAtRollover = time.localtime(newRolloverAt)[-1]
            dstNow = time.localtime(currentTime)[-1]
            if dstNow != dstAtRollover:
                if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
                    addend = -3600
                else:           # DST bows out before next rollover, so we need to add an hour
                    addend = 3600
                newRolloverAt += addend
        self.rolloverAt = newRolloverAt

在 settings handles 中引入上面 class 即可。

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