日誌概述
百度百科的日誌概述:
Windows網路作業系統都設計有各種各樣的日誌檔案,如應用程式日誌,安全日誌、系統日誌、Scheduler服務日誌、FTP日誌、WWW日誌、DNS伺服器日誌等等,這些根據你的系統開啟的服務的不同而有所不同。我們在系統上進行一些操作時,這些日誌檔案通常會記錄下我們操作的一些相關內容,這些內容對系統安全工作人員相當有用。比如說有人對系統進行了IPC探測,系統就會在安全日誌裡迅速地記下探測者探測時所用的IP、時間、使用者名稱等,用FTP探測後,就會在FTP日誌中記下IP、時間、探測所用的使用者名稱等。
我映像中的日誌:
檢視日誌是開發人員日常獲取資訊、排查異常、發現問題的最好途徑,日誌記錄中通常會標記有異常產生的原因、發生時間、具體錯誤行數等資訊,這極大的節省了我們的排查時間,無形中提高了編碼效率。
日誌分類
我們可以按照輸出終端進行分類,也可以按照日誌級別進行分類。輸出終端指的是將日誌在控制檯輸出顯示和將日誌存入檔案;日誌級別指的是 Debug、Info、WARNING、ERROR以及CRITICAL等嚴重等級進行劃分。
Python 的 logging
logging提供了一組便利的日誌函式,它們分別是:debug()、 info()、 warning()、 error() 和 critical()。logging函式根據它們用來跟蹤的事件的級別或嚴重程度來命名。標準級別及其適用性描述如下(以嚴重程度遞增排序):
每個級別對應的數字值為 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、共同進步')
複製程式碼
從上圖執行的結果來看,的確只顯示了 WARNING 級別的資訊,驗證了上面的觀點。同時也在控制檯輸出了日誌內容,預設情況下 Python 中使用 logging 模組中的函式列印日誌,日誌只會在控制檯輸出,而不會儲存到日檔案。
有什麼辦法可以改變預設的日誌級別呢?
當然是有的,logging 中提供了 basicConfig 讓使用者可以適時調節預設日誌級別,我們可以將上面的程式碼改為:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug('崔慶才丨靜覓、韋世東丨奎因')
logging.warning('邀請你關注微信公眾號【進擊的 Coder】')
logging.info('和大佬一起coding、共同進步')
複製程式碼
在 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、共同進步')
複製程式碼
在配置中填寫 filename (指定檔名) 和 filemode (檔案寫入方式),控制檯的日誌輸出就不見了,那麼 coder.log 會生成麼?
在 .py 檔案的同級目錄生成了名為 coder.log 的日誌。
通過簡單的程式碼設定,我們就完成了日誌檔案在控制檯和檔案中的輸出。那既在控制檯顯示又能儲存到檔案中呢?
強大的 logging
logging所提供的模組級別的日誌記錄函式是對logging日誌系統相關類的封裝
logging 模組提供了兩種記錄日誌的方式:
- 使用logging提供的模組級別的函式
- 使用Logging日誌系統的四大元件
這裡提到的級別函式就是上面所用的 DEBGE、ERROR 等級別,而四大元件則是指 loggers、handlers、filters 和 formatters 這幾個元件,下圖簡單明瞭的闡述了它們各自的作用:
日誌器(logger)是入口,真正工作的是處理器(handler),處理器(handler)還可以通過過濾器(filter)和格式器(formatter)對要輸出的日誌內容做過濾和格式化等處理操作。四大元件
下面介紹下與logging四大元件相關的類:Logger, Handler, Filter, Formatter。
Logger類
Logger 物件有3個工作要做:
1)嚮應用程式程式碼暴露幾個方法,使應用程式可以在執行時記錄日誌訊息;
2)基於日誌嚴重等級(預設的過濾設施)或filter物件來決定要對哪些日誌進行後續處理;
3)將日誌訊息傳送給所有感興趣的日誌handlers。
複製程式碼
Logger物件最常用的方法分為兩類:配置方法 和 訊息傳送方法
最常用的配置方法如下:
關於Logger.setLevel()方法的說明:
內建等級中,級別最低的是DEBUG,級別最高的是CRITICAL。例如setLevel(logging.INFO),此時函式引數為INFO,那麼該logger將只會處理INFO、WARNING、ERROR和CRITICAL級別的日誌,而DEBUG級別的訊息將會被忽略/丟棄。
logger物件配置完成後,可以使用下面的方法來建立日誌記錄:
那麼,怎樣得到一個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方法就是下面這幾個配置方法:
需要說明的是,應用程式程式碼不應該直接例項化和使用Handler例項。因為Handler是一個基類,它只定義了素有handlers都應該有的介面,同時提供了一些子類可以直接使用或覆蓋的預設行為。下面是一些常用的Handler: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數量等。
複製程式碼
實戰演練
上面文縐縐的說了(複製/貼上)那麼多,現在應該動手實踐了。
現在我需要既將日誌輸出到控制檯、又能將日誌儲存到檔案,我應該怎麼辦?
利用剛才所學的知識,我們可以構思一下:
看起來好像也不難,挺簡單的樣子,但是實際如此嗎?
在實際的工作或應用中,我們或許還需要指定檔案存放路徑、用隨機數作為日誌檔名、顯示具體的資訊輸出程式碼行數、日誌資訊輸出日期和日誌寫入方式等內容。再構思一下:
具體程式碼如下: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)
複製程式碼
檔案儲存後執行,執行結果如下圖所示:
日誌確實在控制檯輸出了,再來看一下目錄內是否生成有指定的檔案和資料夾:
檔案開啟後可以看到裡面輸出的內容:
正確的學習方式是什麼
是一步步的看著文章介紹,等待博主結論?
是拿著程式碼執行,跑一遍?
都不是,應該是一邊看著文章,一邊拿著示例程式碼琢磨和研究,到底哪裡可以改進、哪裡可以設計得更好。如果你需要文章中所用到的示例程式碼和流程圖,那麼關注微信公眾號【進擊的 Coder】,回覆『日誌程式碼』就可以領取文章中完整的程式碼以及流程圖。畢竟,學習是一件勤勞的事。
參考資料: