一看就懂,Python 日誌模組詳解及應用

進擊的Coder發表於2018-11-20

日誌概述

百度百科的日誌概述

Windows網路作業系統都設計有各種各樣的日誌檔案,如應用程式日誌,安全日誌、系統日誌、Scheduler服務日誌、FTP日誌、WWW日誌、DNS伺服器日誌等等,這些根據你的系統開啟的服務的不同而有所不同。我們在系統上進行一些操作時,這些日誌檔案通常會記錄下我們操作的一些相關內容,這些內容對系統安全工作人員相當有用。比如說有人對系統進行了IPC探測,系統就會在安全日誌裡迅速地記下探測者探測時所用的IP、時間、使用者名稱等,用FTP探測後,就會在FTP日誌中記下IP、時間、探測所用的使用者名稱等。

我映像中的日誌

檢視日誌是開發人員日常獲取資訊、排查異常、發現問題的最好途徑,日誌記錄中通常會標記有異常產生的原因、發生時間、具體錯誤行數等資訊,這極大的節省了我們的排查時間,無形中提高了編碼效率。

日誌分類

我們可以按照輸出終端進行分類,也可以按照日誌級別進行分類。輸出終端指的是將日誌在控制檯輸出顯示和將日誌存入檔案;日誌級別指的是 Debug、Info、WARNING、ERROR以及CRITICAL等嚴重等級進行劃分。

Python 的 logging

logging提供了一組便利的日誌函式,它們分別是:debug()、 info()、 warning()、 error() 和 critical()。logging函式根據它們用來跟蹤的事件的級別或嚴重程度來命名。標準級別及其適用性描述如下(以嚴重程度遞增排序):

一看就懂,Python 日誌模組詳解及應用

每個級別對應的數字值為 CRITICAL:50,ERROR:40,WARNING:30,INFO:20,DEBUG:10,NOTSET:0。 Python 中日誌的預設等級是 WARNING,DEBUG 和 INFO 級別的日誌將不會得到顯示,在 logging 中更改設定。

日誌輸出

輸出到控制檯

使用 logging 在控制檯列印日誌,這裡我們用 Pycharm 編輯器來觀察:

import logging


logging.debug('崔慶才丨靜覓、韋世東丨奎因')
logging.warning('邀請你關注微信公眾號【進擊的 Coder】')
logging.info('和大佬一起coding、共同進步')

複製程式碼

一看就懂,Python 日誌模組詳解及應用
從上圖執行的結果來看,的確只顯示了 WARNING 級別的資訊,驗證了上面的觀點。同時也在控制檯輸出了日誌內容,預設情況下 Python 中使用 logging 模組中的函式列印日誌,日誌只會在控制檯輸出,而不會儲存到日檔案。

有什麼辦法可以改變預設的日誌級別呢?

當然是有的,logging 中提供了 basicConfig 讓使用者可以適時調節預設日誌級別,我們可以將上面的程式碼改為:

import logging

logging.basicConfig(level=logging.DEBUG)
logging.debug('崔慶才丨靜覓、韋世東丨奎因')
logging.warning('邀請你關注微信公眾號【進擊的 Coder】')
logging.info('和大佬一起coding、共同進步')
複製程式碼

一看就懂,Python 日誌模組詳解及應用

在 basicConfig 中設定 level 引數的級別即可。

思考:如果設定級別為 logging.INFO,那 DEBUG 資訊能夠顯示麼?

儲存到檔案

剛才演示瞭如何在控制檯輸出日誌內容,並且自由設定日誌的級別,那現在就來看看如何將日誌儲存到檔案。依舊是強大的 basicConfig,我們再將上面的程式碼改為:

import logging

logging.basicConfig(level=logging.DEBUG, filename='coder.log', filemode='a')
logging.debug('崔慶才丨靜覓、韋世東丨奎因')
logging.warning('邀請你關注微信公眾號【進擊的 Coder】')
logging.info('和大佬一起coding、共同進步')

複製程式碼

一看就懂,Python 日誌模組詳解及應用
在配置中填寫 filename (指定檔名) 和 filemode (檔案寫入方式),控制檯的日誌輸出就不見了,那麼 coder.log 會生成麼?

一看就懂,Python 日誌模組詳解及應用
在 .py 檔案的同級目錄生成了名為 coder.log 的日誌。

通過簡單的程式碼設定,我們就完成了日誌檔案在控制檯和檔案中的輸出。那既在控制檯顯示又能儲存到檔案中呢?

強大的 logging

logging所提供的模組級別的日誌記錄函式是對logging日誌系統相關類的封裝

logging 模組提供了兩種記錄日誌的方式:

  • 使用logging提供的模組級別的函式
  • 使用Logging日誌系統的四大元件

這裡提到的級別函式就是上面所用的 DEBGE、ERROR 等級別,而四大元件則是指 loggers、handlers、filters 和 formatters 這幾個元件,下圖簡單明瞭的闡述了它們各自的作用:

一看就懂,Python 日誌模組詳解及應用
日誌器(logger)是入口,真正工作的是處理器(handler),處理器(handler)還可以通過過濾器(filter)和格式器(formatter)對要輸出的日誌內容做過濾和格式化等處理操作。

四大元件

下面介紹下與logging四大元件相關的類:Logger, Handler, Filter, Formatter。

一看就懂,Python 日誌模組詳解及應用

Logger類

Logger 物件有3個工作要做:

1)嚮應用程式程式碼暴露幾個方法,使應用程式可以在執行時記錄日誌訊息;
2)基於日誌嚴重等級(預設的過濾設施)或filter物件來決定要對哪些日誌進行後續處理;
3)將日誌訊息傳送給所有感興趣的日誌handlers。
複製程式碼

Logger物件最常用的方法分為兩類:配置方法 和 訊息傳送方法

最常用的配置方法如下:

一看就懂,Python 日誌模組詳解及應用

關於Logger.setLevel()方法的說明:

內建等級中,級別最低的是DEBUG,級別最高的是CRITICAL。例如setLevel(logging.INFO),此時函式引數為INFO,那麼該logger將只會處理INFO、WARNING、ERROR和CRITICAL級別的日誌,而DEBUG級別的訊息將會被忽略/丟棄。

logger物件配置完成後,可以使用下面的方法來建立日誌記錄:

一看就懂,Python 日誌模組詳解及應用
那麼,怎樣得到一個Logger物件呢?一種方式是通過Logger類的例項化方法建立一個Logger類的例項,但是我們通常都是用第二種方式--logging.getLogger()方法。

logging.getLogger()方法有一個可選引數name,該參數列示將要返回的日誌器的名稱標識,如果不提供該引數,則其值為'root'。若以相同的name引數值多次呼叫getLogger()方法,將會返回指向同一個logger物件的引用。

關於logger的層級結構與有效等級的說明:

    logger的名稱是一個以'.'分割的層級結構,每個'.'後面的logger都是'.'前面的logger的children,例如,有一個名稱為 foo 的logger,其它名稱分別為 foo.bar, foo.bar.baz 和 foo.bam都是 foo 的後代。
    logger有一個"有效等級(effective level)"的概念。如果一個logger上沒有被明確設定一個level,那麼該logger就是使用它parent的level;如果它的parent也沒有明確設定level則繼續向上查詢parent的parent的有效level,依次類推,直到找到個一個明確設定了level的祖先為止。需要說明的是,root logger總是會有一個明確的level設定(預設為 WARNING)。當決定是否去處理一個已發生的事件時,logger的有效等級將會被用來決定是否將該事件傳遞給該logger的handlers進行處理。
    child loggers在完成對日誌訊息的處理後,預設會將日誌訊息傳遞給與它們的祖先loggers相關的handlers。因此,我們不必為一個應用程式中所使用的所有loggers定義和配置handlers,只需要為一個頂層的logger配置handlers,然後按照需要建立child loggers就可足夠了。我們也可以通過將一個logger的propagate屬性設定為False來關閉這種傳遞機制。
複製程式碼

Handler

Handler物件的作用是(基於日誌訊息的level)將訊息分發到handler指定的位置(檔案、網路、郵件等)。Logger物件可以通過addHandler()方法為自己新增0個或者更多個handler物件。比如,一個應用程式可能想要實現以下幾個日誌需求:

1)把所有日誌都傳送到一個日誌檔案中;
2)把所有嚴重級別大於等於error的日誌傳送到stdout(標準輸出);
3)把所有嚴重級別為critical的日誌傳送到一個email郵件地址。
這種場景就需要3個不同的handlers,每個handler複雜傳送一個特定嚴重級別的日誌到一個特定的位置。
複製程式碼

一個handler中只有非常少數的方法是需要應用開發人員去關心的。對於使用內建handler物件的應用開發人員來說,似乎唯一相關的handler方法就是下面這幾個配置方法:

一看就懂,Python 日誌模組詳解及應用
需要說明的是,應用程式程式碼不應該直接例項化和使用Handler例項。因為Handler是一個基類,它只定義了素有handlers都應該有的介面,同時提供了一些子類可以直接使用或覆蓋的預設行為。下面是一些常用的Handler:

一看就懂,Python 日誌模組詳解及應用

Formater

Formater物件用於配置日誌資訊的最終順序、結構和內容。與logging.Handler基類不同的是,應用程式碼可以直接例項化Formatter類。另外,如果你的應用程式需要一些特殊的處理行為,也可以實現一個Formatter的子類來完成。

Formatter類的構造方法定義如下:

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
複製程式碼

該構造方法接收3個可選引數:

  • fmt:指定訊息格式化字串,如果不指定該引數則預設使用message的原始值
  • datefmt:指定日期格式字串,如果不指定該引數則預設使用"%Y-%m-%d %H:%M:%S"
  • style:Python 3.2新增的引數,可取值為 '%', '{'和 '$',如果不指定該引數則預設使用'%'

Filter

Filter可以被Handler和Logger用來做比level更細粒度的、更復雜的過濾功能。Filter是一個過濾器基類,它只允許某個logger層級下的日誌事件通過過濾。該類定義如下:

class logging.Filter(name='')
    filter(record)
複製程式碼

比如,一個filter例項化時傳遞的name引數值為'A.B',那麼該filter例項將只允許名稱為類似如下規則的loggers產生的日誌記錄通過過濾:'A.B','A.B,C','A.B.C.D','A.B.D',而名稱為'A.BB', 'B.A.B'的loggers產生的日誌則會被過濾掉。如果name的值為空字串,則允許所有的日誌事件通過過濾。

filter方法用於具體控制傳遞的record記錄是否能通過過濾,如果該方法返回值為0表示不能通過過濾,返回值為非0表示可以通過過濾。

說明:

    如果有需要,也可以在filter(record)方法內部改變該record,比如新增、刪除或修改一些屬性。
    我們還可以通過filter做一些統計工作,比如可以計算下被一個特殊的logger或handler所處理的record數量等。
複製程式碼

實戰演練

上面文縐縐的說了(複製/貼上)那麼多,現在應該動手實踐了。

現在我需要既將日誌輸出到控制檯、又能將日誌儲存到檔案,我應該怎麼辦?

利用剛才所學的知識,我們可以構思一下:

一看就懂,Python 日誌模組詳解及應用

看起來好像也不難,挺簡單的樣子,但是實際如此嗎?

在實際的工作或應用中,我們或許還需要指定檔案存放路徑、用隨機數作為日誌檔名、顯示具體的資訊輸出程式碼行數、日誌資訊輸出日期和日誌寫入方式等內容。再構思一下:

一看就懂,Python 日誌模組詳解及應用
具體程式碼如下:

import os
import logging
import uuid
from logging import Handler, FileHandler, StreamHandler


class PathFileHandler(FileHandler):
    def __init__(self, path, filename, mode='a', encoding=None, delay=False):

        filename = os.fspath(filename)
        if not os.path.exists(path):
            os.mkdir(path)
        self.baseFilename = os.path.join(path, filename)
        self.mode = mode
        self.encoding = encoding
        self.delay = delay
        if delay:
            Handler.__init__(self)
            self.stream = None
        else:
            StreamHandler.__init__(self, self._open())


class Loggers(object):
    # 日誌級別關係對映
    level_relations = {
        'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING,
        'error': logging.ERROR, 'critical': logging.CRITICAL
    }

    def __init__(self, filename='{uid}.log'.format(uid=uuid.uuid4()), level='info', log_dir='log',
                 fmt='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s'):
        self.logger = logging.getLogger(filename)
        abspath = os.path.dirname(os.path.abspath(__file__))
        self.directory = os.path.join(abspath, log_dir)
        format_str = logging.Formatter(fmt)  # 設定日誌格式
        self.logger.setLevel(self.level_relations.get(level))  # 設定日誌級別
        stream_handler = logging.StreamHandler()  # 往螢幕上輸出
        stream_handler.setFormatter(format_str)
        file_handler = PathFileHandler(path=self.directory, filename=filename, mode='a')
        file_handler.setFormatter(format_str)
        self.logger.addHandler(stream_handler)
        self.logger.addHandler(file_handler)


if __name__ == "__main__":
    txt = "關注公眾號【進擊的 Coder】,回覆『日誌程式碼』可以領取文章中完整的程式碼以及流程圖"
    log = Loggers(level='debug')
    log.logger.info(4)
    log.logger.info(5)
    log.logger.info(txt)

複製程式碼

檔案儲存後執行,執行結果如下圖所示:

一看就懂,Python 日誌模組詳解及應用

日誌確實在控制檯輸出了,再來看一下目錄內是否生成有指定的檔案和資料夾:

一看就懂,Python 日誌模組詳解及應用

檔案開啟後可以看到裡面輸出的內容:

一看就懂,Python 日誌模組詳解及應用

正確的學習方式是什麼

是一步步的看著文章介紹,等待博主結論?

是拿著程式碼執行,跑一遍?

都不是,應該是一邊看著文章,一邊拿著示例程式碼琢磨和研究,到底哪裡可以改進、哪裡可以設計得更好。如果你需要文章中所用到的示例程式碼和流程圖,那麼關注微信公眾號【進擊的 Coder】,回覆『日誌程式碼』就可以領取文章中完整的程式碼以及流程圖。畢竟,學習是一件勤勞的事。

一看就懂,Python 日誌模組詳解及應用

參考資料:

雲遊道士博文

nancy05博文

相關文章