Python 記錄日志的靈活性和可配置性介紹
對一名開發(fā)者來說最糟糕的情況,莫過于要弄清楚一個不熟悉的應(yīng)用為何不工作。有時候,你甚至不知道系統(tǒng)運行,是否跟原始設(shè)計一致。
在線運行的應(yīng)用就是黑盒子,需要被跟蹤監(jiān)控。最簡單也最重要的方式就是記錄日志。記錄日志允許我們在開發(fā)軟件的同時,讓程序在系統(tǒng)運行時發(fā)出信息,這些信息對于我們和系統(tǒng)管理員來說都是有用的。
就像為將來的程序員寫代碼文檔一樣,我們應(yīng)該讓新軟件產(chǎn)生足夠的日志供系統(tǒng)的開發(fā)者和管理員使用。日志是關(guān)于應(yīng)用運行狀態(tài)的系統(tǒng)文件的關(guān)鍵部分。給軟件加日志產(chǎn)生句時,要向給未來維護(hù)系統(tǒng)的開發(fā)者和管理員寫文檔一樣。
一些純粹主義者認(rèn)為一個受過訓(xùn)練的開發(fā)者使用日志和測試的時候幾乎不需要交互調(diào)試器。如果我們不能用詳細(xì)的日志解釋開發(fā)過程中的應(yīng)用,那么當(dāng)代碼在線上運行的時候,解釋它們會變得更困難。
這篇文章介紹了 Python 的 logging 模塊,包括它的設(shè)計以及針對更多復(fù)雜案例的適用方法。這篇文章不是寫給開發(fā)者的文檔,它更像是一個指導(dǎo)手冊,來說明 Python 的 logging 模板是如何搭建的,并且激發(fā)感興趣的人深入研究。
為什么使用 logging 模塊?
也許會有開發(fā)者會問,為什么不是簡單的 print 語句呢? Logging 模塊有很多優(yōu)勢,包括:
1.多線程支持
2.通過不同級別的日志分類
3.靈活性和可配置性
4.將如何記錄日志與記錄什么內(nèi)容分離
最后一點,將我們記錄內(nèi)容從記錄方式中真正分離,保證了軟件不同部分的合作。舉個例子,它允許一個框架或庫的開發(fā)者增加日志并且讓系統(tǒng)管理員或負(fù)責(zé)運行配置的人員決定稍后應(yīng)該記錄什么。
Logging 模塊中有什么
Logging 模塊完美地將它的每個部分的職責(zé)分離(遵循 Apache Log4j API 的方法)。讓我們看看一個日志線是如何通過這個模塊的代碼,并且研究下它的不同部分。
記錄器(Logger)
記錄器是開發(fā)者經(jīng)常交互的對象。那些主要的 API 說明了我們想要記錄的內(nèi)容。
舉個記錄器的例子,我們可以分類請求發(fā)出一條信息,而不用擔(dān)心它們是如何從哪里被發(fā)出的。
比如,當(dāng)我們寫下 logger.info(“Stock was sold at %s”, price)
我們在頭腦中就有如下模塊:
我們需要一條線。假設(shè)有些代碼在記錄器中運行,讓這條線出現(xiàn)在控制臺或文件中。但是在內(nèi)部實際發(fā)生了什么呢?
日志記錄
日志記錄是 logging 模塊用來滿足所有需求信息的包。它們包含了需要記錄日志的地方、變化的字符串、參數(shù)、請求的信息隊列等信息。
它們都是被記錄的對象。每次我們調(diào)用記錄器時,都會生成這些對象。但這些對象是如何序列化到流中的呢?通過處理器!
處理器
處理器將日志記錄發(fā)送給其他輸出終端,他們獲取日志記錄并用相關(guān)函數(shù)中處理它們。
比如,一個文件處理器將會獲取一條日志記錄,并且把它添加到文件中。
標(biāo)準(zhǔn)的 logging 模塊已經(jīng)具備了多種內(nèi)置的處理器,例如:
多種文件處理器(TimeRotated, SizeRotated, Watched),可以寫入文件中
1.StreamHandler 輸出目標(biāo)流比如 stdout 或 stderr
2.SMTPHandler 通過 email 發(fā)送日志記錄
3.SocketHandler 將日志文件發(fā)送到流套接字
4.SyslogHandler、NTEventHandler、HTTPHandler及MemoryHandler等
目前我們有個類似于真實情況的模型:
大部分的處理器都在處理字符串(SMTPHandler和FileHandler等)。或許你想知道這些結(jié)構(gòu)化的日志記錄是如何轉(zhuǎn)變?yōu)橐子谛蛄谢淖止?jié)的。
格式器
格式器負(fù)責(zé)將豐富的元數(shù)據(jù)日志記錄轉(zhuǎn)換為字符串,如果什么都沒有提供,將會有個默認(rèn)的格式器。
一般的格式器類由 logging 庫提供,采用模板和風(fēng)格作為輸入。然后占位符可以在一個 LogRecord 對象中聲明所有屬性。
比如:'%(asctime)s %(levelname)s %(name)s: %(message)s' 將會生成日志類似于 2017-07-19 15:31:13,942 INFO parent.child: Hello EuroPython.
請注意:屬性信息是通過提供的參數(shù)對日志的原始模板進(jìn)行插值的結(jié)果。(比如,對于 logger.info(“Hello %s”, “Laszlo”) 這條信息將會是 “Hello Laszlo”)
所有默認(rèn)的屬性都可以在日志文檔中找到。
好了,現(xiàn)在我們了解了格式器,我們的模型又發(fā)生了變化:
過濾器
我們?nèi)罩竟ぞ叩淖詈笠粋€對象就是過濾器。
過濾器允許對應(yīng)該發(fā)送的日志記錄進(jìn)行細(xì)粒度控制。多種過濾器能同時應(yīng)用在記錄器和處理器中。對于一條發(fā)送的日志來說,所有的過濾器都應(yīng)該通過這條記錄。
用戶可以聲明他們自己的過濾器作為對象,使用 filter 方法獲取日志記錄作為輸入,反饋 True / False 作為輸出。
出于這種考慮,以下是當(dāng)前的日志工作流:
記錄器層級
此時,你可能會對大量復(fù)雜的內(nèi)容和巧妙隱藏的模塊配置印象深刻,但是還有更需要考慮的:記錄器分層。
我們可以通過 logging.getLogger()
創(chuàng)建一個記錄器。這條字符向 getLogger 傳遞了一個參數(shù),這個參數(shù)可以通過使用圓點分隔元素來定義一個層級。
舉個例子,logging.getLogger(“parent.child”)
將會創(chuàng)建一個 “child” 的記錄器,它的父級記錄器叫做 “parent.” 記錄器是被 logging 模塊管理的全局對象,所以我們可以方便地在項目中的任何地方檢索他們。
記錄器的例子通常也被認(rèn)為是渠道。層級允許開發(fā)者去定義渠道和他們的層級。
在日志記錄被傳遞到所有記錄器內(nèi)的處理器時,父級處理器將會進(jìn)行遞歸處理,直到我們到達(dá)頂級的記錄器(被定義為一個空字符串),或者有一個記錄器設(shè)置了 propagate = False。我們可通過更新的圖中看出:
請注意父級記錄器沒有被調(diào)用,只有它的處理器被調(diào)用。這意味著過濾器和其他在記錄器類中的代碼不會在父級中被執(zhí)行。當(dāng)我們在記錄器中增加過濾器時,這通常是個陷阱。
工作流小結(jié)
我們已經(jīng)闡明過職責(zé)的劃分以及我們是如何微調(diào)日志過濾。然而還是有兩個其他的屬性我們沒有提及:
1.記錄器可以是殘缺的,從而不允許任何記錄從這被發(fā)出。
2.一個有效的層級可以同時在記錄器和處理器中被設(shè)置。
舉個例子,當(dāng)一個記錄器被設(shè)置為 INFO 的等級,只有 INFO 等級及以上的才會被傳遞,同樣的規(guī)則適用于處理器。
基于以上所有的考慮,最后的日志記錄的流程圖看起來像這樣:
如何使用日志記錄模塊
現(xiàn)在我們已經(jīng)了解了 logging 模塊的部分及設(shè)計,是時候去了解一個開發(fā)者是如何與它交互的了。以下是一個代碼例子:
import logging def sample_function(secret_parameter): logger = logging.getLogger(__name__) # __name__=projectA.moduleB logger.debug("Going to perform magic with '%s'", secret_parameter) ... try: result = do_magic(secret_parameter) except IndexError: logger.exception("OMG it happened again, someone please tell Laszlo") except: logger.info("Unexpected exception", exc_info=True) raise else: logger.info("Magic with '%s' resulted in '%s'", secret_parameter, result, stack_info=True)
它用模塊 __name__ 創(chuàng)建了一個日志記錄器。它會基于項目結(jié)構(gòu)創(chuàng)建渠道和等級,正如 Pyhon 模塊用圓點連接一樣。
記錄器變量引用記錄器的 “module” ,用 “projectA” 作為父級, “root” 作為父級的父級。
在第五行,我們看到如何執(zhí)行調(diào)用去發(fā)送日志。我們可以用 debug 、 info 、error 或 critical 這些方法之一在合適的等級上去記錄日志。
當(dāng)記錄一條信息時,除了模板參數(shù),我們可以通過特殊的含義傳遞密碼參數(shù),最有意思的是 exc_info 和 stack_info。它們將會分別增加關(guān)于當(dāng)前異常和棧幀的信息。為了方便起見,在記錄器對象中有一個方法異常,正如這個錯誤調(diào)用 exc_info=True 。
這些是如何使用記錄器模塊的基礎(chǔ),但是有些通常被認(rèn)為是不良操作的做法同樣值得說明。
過度格式化字符串
應(yīng)該盡量避免使用 loggger.info(“string template {}”.format(argument))
,可能的話盡量使用 logger.info(“string template %s”, argument)
。 這是個更好的實踐,因為只有當(dāng)日志被發(fā)送時,字符串才會發(fā)生真正改變。當(dāng)我們記錄的層級在 INFO 之上時,不這么做會導(dǎo)致浪費周期,因為這個改變?nèi)匀粫l(fā)生。
捕捉和格式化異常
通常,我們想記錄在抓取模塊異常的日志信息,如果這樣寫會很直觀:
try: .. except Exception as error: logger.info("Something bad happened: %s", error)
但是這樣的代碼會給我們顯示類似于 Something bad happened: “secret_key.”
的日志行,這并不是很有用。如果我們使用 exc_info 作為事先說明,那么它將會如下顯示:
try: .. except Exception: logger.info("Something bad happened", exc_info=True) Something bad happened Traceback (most recent call last): File "sample_project.py", line 10, in code inner_code() File "sample_project.py", line 6, in inner_code x = data["secret_key"] KeyError: 'secret_key'
這不僅僅會包含異常的準(zhǔn)確資源,同時也會包含它的類型。
設(shè)置記錄器
裝備我們的軟件很簡單,我們需要設(shè)置日志棧,并且制定這些記錄是如何被發(fā)出的。
以下是設(shè)置日志棧的多種方法
基礎(chǔ)設(shè)置
這是至今最簡單的設(shè)置日志記錄的方法。使用 logging.basicConfig(level=”INFO”) 搭建一個基礎(chǔ)的 StreamHandler ,這樣就會記錄在 INFO 上的任何東西,并且到控制臺以上的級別。以下是編寫基礎(chǔ)設(shè)置的一些參數(shù):
請注意, basicConfig 僅僅在運行的一開始可以這么調(diào)用。如果你已經(jīng)設(shè)置了你的根記錄器,調(diào)用 basicConfig 將不會奏效。
字典設(shè)置
所有元素的設(shè)置以及如何連接它們可以作為字典來說明。這個字典應(yīng)當(dāng)由不同的部分組成,包括記錄器、處理器、格式化以及一些基本的通用參數(shù)。
例子如下:
config = { 'disable_existing_loggers': False, 'version': 1, 'formatters': { 'short': { 'format': '%(asctime)s %(levelname)s %(name)s: %(message)s' }, }, 'handlers': { 'console': { 'level': 'INFO', 'formatter': 'short', 'class': 'logging.StreamHandler', }, }, 'loggers': { '': { 'handlers': ['console'], 'level': 'ERROR', }, 'plugins': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False } }, } import logging.config logging.config.dictConfig(config)
當(dāng)被引用時, dictConfig 將會禁用所有運行的記錄器,除非 disable_existing_loggers 被設(shè)置為 false。這通常是需要的,因為很多模塊聲明了一個全球記錄器,它在 dictConfig 被調(diào)用之前被導(dǎo)入的時候?qū)嵗?/p>
你可以查看 schema that can be used for the dictConfig method(鏈接)。通常,這些設(shè)置將會存儲在一個 YAML 文件中,并且從那里設(shè)置。很多開發(fā)者會傾向于使用這種方式而不是使用 fileConfig(鏈接),因為它為定制化提供了更好的支持。
拓展 logging
幸虧設(shè)計了這種方式,拓展 logging 模塊很容易。讓我們來看些例子:
logging JSON | 記錄 JSON
只要我們想要記錄,我們可以通過創(chuàng)建一種自定義格式化來記錄 JSON ,它會將日志記錄轉(zhuǎn)化為 JSON 編碼的字符串。
import logging import logging.config import json ATTR_TO_JSON = ['created', 'filename', 'funcName', 'levelname', 'lineno', 'module', 'msecs', 'msg', 'name', 'pathname', 'process', 'processName', 'relativeCreated', 'thread', 'threadName'] class JsonFormatter: def format(self, record): obj = {attr: getattr(record, attr) for attr in ATTR_TO_JSON} return json.dumps(obj, indent=4) handler = logging.StreamHandler() handler.formatter = JsonFormatter() logger = logging.getLogger(__name__) logger.addHandler(handler) logger.error("Hello")
添加更多上下文
在格式化中,我們可以指定任何日志記錄的屬性。
我們可以通過多種方式增加屬性,在這個例子中,我們用過濾器來豐富日志記錄。
import logging import logging.config GLOBAL_STUFF = 1 class ContextFilter(logging.Filter): def filter(self, record): global GLOBAL_STUFF GLOBAL_STUFF += 1 record.global_data = GLOBAL_STUFF return True handler = logging.StreamHandler() handler.formatter = logging.Formatter("%(global_data)s %(message)s") handler.addFilter(ContextFilter()) logger = logging.getLogger(__name__) logger.addHandler(handler) logger.error("Hi1") logger.error("Hi2")
這樣有效地在所有日志記錄中增加了一個屬性,它可以通過記錄器。格式化會在日志行中包含這個屬性。
請注意這會在你的應(yīng)用中影響所有的日志記錄,包含你可能用到以及你發(fā)送日志的庫和其他的框架。它可以用來記錄類似于在所有日志行里的一個獨立請求 ID ,去追蹤請求或者去添加額外的上下文信息。
從 Python 3.2 開始,你可以使用 setLogRecordFactory 去獲得所有日志的創(chuàng)建記錄和增加額外的信息。這個 extra attribute 和 LoggerAdapter class 或許同樣是有趣的。
緩沖日志
有時候當(dāng)錯誤發(fā)生時,我們想要排除日志故障。創(chuàng)建一個緩沖的處理器,來記錄當(dāng)錯誤發(fā)生時的最新故障信息是一種可行的辦法。下面的代碼是個非人為策劃的例子:
import logging import logging.handlers class SmartBufferHandler(logging.handlers.MemoryHandler): def __init__(self, num_buffered, *args, **kwargs): kwargs["capacity"] = num_buffered + 2 # +2 one for current, one for prepop super().__init__(*args, **kwargs) def emit(self, record): if len(self.buffer) == self.capacity - 1: self.buffer.pop(0) super().emit(record) handler = SmartBufferHandler(num_buffered=2, target=logging.StreamHandler(), flushLevel=logging.ERROR) logger = logging.getLogger(__name__) logger.setLevel("DEBUG") logger.addHandler(handler) logger.error("Hello1") logger.debug("Hello2") # This line won't be logged logger.debug("Hello3") logger.debug("Hello4") logger.error("Hello5") # As error will flush the buffered logs, the two last debugs will be logged
總結(jié)
以上所述是小編給大家介紹的Python 記錄日志的靈活性和可配置性,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
Python實現(xiàn)周日歷與時間相互轉(zhuǎn)換
周日歷是日常生活中不常用到的歷法系統(tǒng),一般用于政府、商務(wù)的會計年度或者學(xué)校教學(xué)日歷中。本文為大家介紹了如何利用Python語言實現(xiàn)周日歷與時間相互轉(zhuǎn)換,感興趣的可以學(xué)習(xí)一下2022-07-07python文件讀取read及readlines兩種方法使用詳解
這篇文章主要為大家介紹了python文件讀取read及readlines兩種方法的使用示例及區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07使用Python實現(xiàn)在Word文檔中進(jìn)行郵件合并
郵件合并是現(xiàn)代辦公中一項顯著提升效率的技術(shù),它巧妙地將大量個體數(shù)據(jù)與預(yù)設(shè)的文檔模板相結(jié)合,實現(xiàn)了一次性批量生成定制化文檔,下面我們就來看看如何使用Python實現(xiàn)在Word文檔中進(jìn)行郵件合并吧2024-04-04Python+OpenCV實戰(zhàn)之實現(xiàn)文檔掃描
這篇文章主要為大家詳細(xì)介紹了Python+Opencv如何實現(xiàn)文檔掃描的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-09-09Python學(xué)習(xí)筆記之os模塊使用總結(jié)
這篇文章主要介紹了Python學(xué)習(xí)筆記之os模塊使用總結(jié),本文總結(jié)了多個常用方法,需要的朋友可以參考下2014-11-11Python?Pygame繪制直線實現(xiàn)光線反射效果
這篇文章主要為大家詳細(xì)介紹了如何利用Python?Pygame繪制直線以實現(xiàn)光線反射效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11快速解釋如何使用pandas的inplace參數(shù)的使用
這篇文章主要介紹了快速解釋如何使用pandas的inplace參數(shù)的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07