一、日志相關概念
日志是一種可以追蹤某些軟件運行時所發生事情的方法。軟件開發人員可以向代碼中調用日志記錄相關的方法來表明發生了某些事情。一個事件可以用一個可包含可選變量數據的消息來描述。此外,事件也有重要性的概念,這個重要性也可以被稱為嚴重級別(level).
1.日志的作用
通過log的分析,可以方便用戶了解系統或軟件、應用的運行情況;如果你的應用log足夠豐富,也可以分析以往用戶的操作行為、類型喜好、地域分布或其他更多信息;如果一個應用的log同時分了多個級別,那么可以輕易地分析得到該應用的健康狀況,及時發現問題并快速定位、解決問題,補救損失。
簡單來講,我們通過記錄和分析日志可以了解一個系統或軟件程序運行情況是否正常,也可以在應用程序出現故障時快速定位問題。比如,做運維的同學,在接收到報警或各種問題反饋后,進行問題排查時通常都會先去看各種日志,大部分問題都可以在日志中找到答案。再比如,做開發的童鞋,可以通過IDE控制臺上輸出的各種日志進行程序調試。對于運維老司機或者有經驗的開發人員,可以快速的通過日志定位問題的根源。可見,日志是非常重要的。
日志的作用主要有以下3點:
- 程序調試
- 了解軟件運行情況是否正常
- 軟件運行故障分析與問題定位
如果應用的日志信息足夠詳細和豐富,還可以用來作用戶行為分析,如:分析用戶的操作行為、類型喜好、地域分布以及其他更多的信息,由此可以實現改進業務、提高商業利益。
2.日志的等級
我們先來思考下面的兩個問題:
- 作為開發人員,在開發一個應用程序時需要什么日志信息?在應用程序正式上線后需要什么日志信息?
- 作為應用運維人員,在部署開發環境時需要什么日志信息?在部署生產環境時需要什么日志信息?
在軟件開發階段或部署開發環境時,為了盡可能詳細的查看應用程序的運行狀態來保證上線后的穩定性,我們可能需要把該應用程序所有的運行日志全部記錄下來進行分析,這是非常耗費機器性能的。當應用程序正式發布或在生產環境部署應用程序時,我們通過只需要記錄應用程序的異常信息、錯誤信息等,這樣既可以減小服務器的I/O壓力,也可以避免我們在排查故障時淹沒在日志的海洋里。那么,怎樣才能在不改動應用程序代碼的情況下實現在不同的環境記錄不同詳細程度的日志呢?這就是日志等級的作用了,我們通過配置文件指定我們需要的日志等級就可以了。
不同的應用程序所定義的日志等級可能會有所差別,分的詳細點的會包含以下幾個等級:
- DEBUG
- INFO
- NOTICE
- WARNING
- ERROR
- CRITICAL
- ALERT
- EMERGENCY
3.日志字段信息與日志格式
本節開始問題提到過,一條日志信息對應的是一個事件的發生,而一個事件通常包括以下幾個內容:
- 事件發生時間
- 事件發生位置
- 事件的嚴重程序--日志級別
- 事件內容
上面這些都是一條日志記錄中可能包含的字段信息,當然還可以包括一些其他信息,如進程ID、進程名稱、線程ID、線程名稱等。日志格式就是用來定義一條日志記錄中包含哪些字段的,且日志格式通常都是可以自定義的。
說明:
輸出一條日志時,日志內容和日志級別是需要開發人員明確指定的。對于其他字段信息,只需要是否顯示在日志中就可以了。
二、logging模塊簡介
logging模塊定義的函數和類為應用程序和庫的開發實現了一個靈活的事件日志系統。logging模塊是python的一個標準庫模塊,由標準庫模塊提供日志記錄API的關鍵好處是所有python模塊都可以使用這個日志記錄功能。所有,你的應用日志可以將你自己的日志信息與來自第三方模塊的信息整合起來。
1.logging模塊的日志級別
logging模塊默認定義了以下幾個日志等級,它允許開發人員自定義其他日志級別,但是這是不被推薦的,尤其是在開發供別人使用的庫時,因為這會導致日志級別的混亂。
日志等級(level)描述
DEBUG:最詳細的日志信息,典型應用場景是問題診斷
INFO:信息詳細程度僅次于DEBUG,通常只記錄關鍵節點信息,用于確認一切都是按照我們預期的那樣進行工作
WARNING:當某些不期望的事情發生時記錄的信息(如,磁盤可用空間較低),但是此時應用程序還是正常運行的
ERROR:由于一個更嚴重的問題導致某些功能不能正常運行時記錄的信息
CRITICAL:當發生嚴重錯誤,導致應用程序不能繼續運行時繼續的信息
開發應用程序或部署開發環境時,可以使用DEBUG或INFO級別的日志獲取盡可能詳細的日志信息來進行開發或部署調試;應用上線或部署生產環境時,應該使用WARNING或ERROR或CRITICAL級別的日志來降低機器的I/O壓力和提高獲取錯誤日志信息的效率。日志級別的指定通常都是在應用程序的配置文件中進行指定的。
說明:
- 上面列表中的日志等級是從上到下依次升高的,即:DEBUG<INFO<WARNING<ERROR<CRITICAL,而日志的信息量是依次減少的;
- 當為某個應用程序指定一個日志級別后,應用程序會記錄所有日志級別大于或等于指定日志級別的日志信息,而不僅僅記錄指定級別的日志信息,nginx、php等應用程序以及這里要提高的python的logging模塊都是這樣的。通過logging模塊也可以指定日志記錄器的日志級別,只有級別大于或等于該指定日志級別的日志記錄才會被輸出,小于該等級的日志記錄將會被丟棄。
2.logging模塊的使用方式介紹
logging模塊提供了兩種記錄日志的方式:
- 第一種方式是使用logging提供的模塊級別的函數
- 第二種方式是使用logging日志系統的四大組件
其實,logging所提供的模塊級別的日志記錄函數也是對logging日志系統相關類的封裝而已。
logging模塊定義的模塊級別的常用函數
函數及說明
logging.debug(msg,args,kwargs):創建一條嚴重級別為DEBUG的日志記錄
logging.info(msg, args, kwargs):創建一條嚴重級別為INFO的日志記錄
logging.warning(msg,args,kwargs):創建一條嚴重級別為WARNING的日志記錄
logging.error(msg,args,kwargs):創建一條嚴重級別為ERROR的日志記錄
logging.critical(msg,args,kwargs):創建一條嚴重級別為CRITICAL的日志記錄
logging.log(level,args,kwargs):創建一條嚴重級別為level的日志記錄
logging.basicConfig(kwargs):對root logger進行一次性配置
其中logging.basicConfig(**kwargs)
函數用于指定“要記錄的日志級別”、“日志格式”、“日志輸出位置”、“日志文件的打開模式”等信息,其他幾個都是用于記錄各個級別日志的函數。
logging模塊的四大組件
組件及說明
loggers:提供應用程序代碼直接使用的接口
handlers:用于將日志記錄發送到指定的目的位置
filters:提供更細粒度的日志過濾功能,用于決定哪些日志記錄將會被輸出(其他的日志記錄將會被忽略)
formatters:用于控制日志信息的最終輸出格式
說明: logging模塊提供的模塊級別的那些函數實際上也是通過這幾個組件的相關實現類來記錄日志的,只是在創建這些類的實例時設置了一些默認值。
三、使用logging提供的模塊級別的函數記錄日志
回顧下前面提到的幾個重要信息:
- 可以通過logging模塊定義的模塊級別的方法去完成簡單的日志記錄
- 只有級別大于或等于日志記錄器指定級別的日志記錄才會被輸出,小于該級別的日志記錄將會被丟棄。
1.最簡單的日志輸出
先來試著分別輸出一條不同日志級別的日志記錄:
import logging
logging.debug("This is a debug log.")
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")
也可以這樣寫:
logging.log(logging.DEBUG, "This is a debug log.")
logging.log(logging.INFO, "This is a info log.")
logging.log(logging.WARNING, "This is a warning log.")
logging.log(logging.ERROR, "This is a error log.")
logging.log(logging.CRITICAL, "This is a critical log.")
輸出結果:
WARNING:root:This is a warning log.
ERROR:root:This is a error log.
CRITICAL:root:This is a critical log.
2.那么問題來了
問題1:為什么前面兩條日志沒有被打印出來?
這是因為logging模塊提供的日志記錄函數所使用的日志器設置的日志級別是WARNING
,因此只有WARNING
級別的日志記錄以及大于它的級別的日志記錄被輸出了,而小于他的則被丟棄了。
問題2:打印出來的日志信息中各字段表示什么意思?為什么會這樣輸出?
上面輸出結果中每行日志記錄的各個字段含義分別是:
日志級別:日志器名稱:日志內容
之所以這樣輸出,是因為logging模塊提供的日志記錄函數所使用的日志器設置的日志格式默認是BASIC_FORMAT,其值為:
''%(levelname)s:%(name)s:%(message)s"
問題3:如何將日志記錄輸出到文件中,而不是打印到控制臺
因為在logging模塊提供的日志記錄函數所使用的日志器設置的處理器所指定的日志輸出位置默認為:
sys.stderr
問題4:我是怎么知道這些的?
查看這些日志記錄函數的實現代碼,可以發現:當我們沒有提供任何配置信息的時候,這些函數都會去調用logging.basicConfig(kwargs)
方法,且不會向該方法傳遞任何參數。繼續查看basicConfig()
方法的代碼就可以找到上面這些問題的答案了。
問題5:怎么修改這些默認設置呢?
在我們調用這些日志記錄函數之前,手動調用一下basicConfig()方法,把我們想設置的內容以參數的形式傳遞進去就可以了。
3.logging.basicConfig()函數說明
該方法用于為logging日志系統做一些基本配置,方法定義如下:
logging.basicConfig(**kwargs)
該函數可接收的關鍵字參數如下:
filename:指定日志輸出目標文件的文件名,指定該設置項后日志信息就不會被輸出到控制臺了
filemode:指定日志文件的打開模式,默認為a,需要注意的是,該選項要在filename指定時才有效
format:指定日志格式字符串,即指定日志輸出時所包含的字段信息以及他們的順序。logging模塊定義的格式字段下面會列出。
datefmt:指定日期/時間格式。需要注意的是,該選項要在format中包含時間字段%(asctime)s時才有效
level:指定日志器的日志級別
stream:指定日志輸出目標stream,如sys.stdout、sys.stderr以及網絡stream。需要說明的是,stream和filename不能同時提供,否則會引發ValueError
異常
style:python3中新添加的配置項。指定format格式字符串的風格,可取%{$,默認為%
handlers:python3中新添加的配置項。該選項如果被指定,它應該是一個創建了多個handler的可迭代對象,這些handler將會被添加到root logger*。需要說明的是:filename、stream和handlers這三個配置項只能有一個存在,不能同時出現兩個以上,否則會引發ValueError異常。
4.logging模塊定義的格式字符串字段
我們來列舉一下logging模塊中定義好的可以用于format格式字符串中字段有哪些:
字段/屬性名稱-使用格式-描述
asctime:%(asctime)s:日志事件發生的時間--人類可讀時間,如:2003-07-08 16:59:59,896
created:%(created)f:日志事件發生的時間--時間戳,就是當時調用time.time()函數返回的值
relativeCreated: %(relatedCreated)d:日志事件發生的時間相對于logging模塊加載時間的相對毫秒數(目前還不知道干嘛用的)
msecs: %(msecs)d:日志事件發生事件的毫秒部分
levelname: %(levelname)s:該日志記錄的文字形式的日志級別('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
levelno: %(levelno)s:該日志記錄的數字形式的日志級別(10,20,30,40,50)
name: %(name)s:所使用的日志器名稱,默認是root,因為默認使用的是rootLogger
message: %(message)s:日志記錄的文本內容,通過msg % args計算得到的
pathname: %(pathname)s:調用日志記錄函數的源碼文件的全路徑
filename: %(filename)s:pathname的文件名部分,包含文件后綴
module: %(module)s:filename的名稱部分,不包含后綴
lineno: %(lineno)d:調用日志記錄函數的源代碼所在的行號
funcName: %(funcName)s:調用日志記錄函數的函數名
process: %(process)d:進程ID
processName: %(processName)s:進程名稱,python3.1新增
thread:%(thread)d:線程ID
threadName: %(threadName)s:線程名稱
5.經過配置的日志輸出
先簡單配置下日志器的日志級別
logging.basicConfig(level=logging.DEBUG)
logging.debug('This is a debug log.')
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")
輸出結果:
DEBUG:root:This is a debug log.
INFO:root:This is a info log.
WARNING:root:This is a warning log.
ERROR:root:This is a error log.
CRITICAL:root:This is a critical log.
所有等級的日志信息都被輸出了,說明配置生效了。
在配置日志器日志級別的基礎上,再配置下日志輸出目標文件和日志格式
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
logging.basicConfig(filename='my.log', level=logging.DEBUG, format=LOG_FORMAT)
logging.debug("This is a debug log.")
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")
此時會發現控制臺中已經沒有輸出日志內容了,但是在python代碼文件的相同目錄下回生成一個名為'my.log'的日志文件,該文件中的內容為:
2017-05-08 14:29:53,783 - DEBUG - This is a debug log.
2017-05-08 14:29:53,784 - INFO - This is a info log.
2017-05-08 14:29:53,784 - WARNING - This is a warning log.
2017-05-08 14:29:53,784 - ERROR - This is a error log.
2017-05-08 14:29:53,784 - CRITICAL - This is a critical log.
在上面的基礎上,我們再來設置下日期/時間格式
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"
logging.basicConfig(filename='my.log', level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)
logging.debug("This is a debug log.")
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")
此時會在my.log日志文件中看到如下輸出內容:
05/08/2017 14:29:04 PM - DEBUG - This is a debug log.
05/08/2017 14:29:04 PM - INFO - This is a info log.
05/08/2017 14:29:04 PM - WARNING - This is a warning log.
05/08/2017 14:29:04 PM - ERROR - This is a error log.
05/08/2017 14:29:04 PM - CRITICAL - This is a critical log.
掌握了上面的內容之后,已經能夠滿足我們平時開發中需要的日志記錄功能。
6.其他說明
幾個要說明的內容:
-
logging.basicConfig()
函數是一個一次性的簡單配置工具,也就是說只有在第一次調用該函數時會起作用,后續再次調用該函數時完全不會產生任何操作的,多次調用的設置并不是累加操作。 - 日志器(Logger)是有層級關系的,上面調用的logging模塊級別的函數所使用的日志器是
RootLogger
類的實例,其名稱為root,它是處于日志器層級關系最頂層的日志器,且該實例是以單例模式存在的。 - 如果要記錄的日志中包含變量數據,可使用一個格式字符串作為這個時間的描述消息(logging.debug、logging.info等函數的第一個參數),然后將變量數據作為第二個參數*args的值進行傳遞,如:
logging.warning('%s is %d years old.', 'Tom', 10)
,輸出內容為:WARNING:root:Tom is 10 years old.
-
logging.debug()、logging.info()等方法的定義中,除了msg和args參數外,還有一個**kwargs參數。它們支持3個關鍵字參數:
exc_info, stack_info, extra
,下面對這幾個關鍵字參數作個說明。
關于exc_info,stack_info,extra關鍵詞參數的說明: - exc_info:其值為布爾值,如果該參數的值設置為True,則會將異常信息添加到日志消息中。如果沒有異常信息則添加NONE到日志信息中。
- stack_info:其值也為布爾值,默認值為False。如果該參數的值設置為True,棧信息將會被添加到日志信息中。
-
extra:這是一個字典參數,它可以用來自定義消息格式中所包含的字段,但是它的key不能與logging模塊定義的字段沖突。
一個例子:
在日志消息中添加exc_info和stack_info信息,并添加兩個自定義的字段IP和user
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(user)s[%(ip)s] - %(message)s"
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"
logging.basicConfig(format=LOG_FORMAT, datefmt=DATE_FORMAT)
logging.warning("Some one delete the log file.", exc_info=True, stack_info=True, extra={'user': 'Tom', 'ip':'47.98.53.222'})
輸出結果:
05/08/2017 16:35:00 PM - WARNING - Tom[47.98.53.222] - Some one delete the log file.
NoneType
Stack (most recent call last):
File "C:/Users/wader/PycharmProjects/LearnPython/day06/log.py", line 45, in <module>
logging.warning("Some one delete the log file.", exc_info=True, stack_info=True, extra={'user': 'Tom', 'ip':'47.98.53.222'})
四、logging模塊日志流處理流程
在介紹logging模塊的高級用法之前,很有必要對logging模塊所包含的重要組件以及其工作流程做個全面、簡要的介紹,這有助于我們更好的理解我們所寫的代碼(將會觸發什么樣的操作)。
1.logging日志模塊四大組件
在介紹logging模塊的日志流處理之前,我們先來介紹下logging模塊的四大組件:
日志器 Logger:提供了應用程序可一直使用的接口
處理器 Handler:將logger創建的日志記錄發送到合適的目的輸出
過濾器 Filter:提供了更細粒度的控制工具來決定輸出哪條日志記錄,丟棄哪條日志記錄
格式器 Formatter:決定日志記錄的最終輸出格式
logging模塊就是通過這些組件來完成日志處理的,上面所使用的logging模塊級別的函數也是通過這些組件對應的類來實現的。
這些組件之間的關系描述:
- 日志器(logger)需要通過處理器(handler)將日志信息輸出到目標位置,如文件、sys.stdout、網絡等;
- 不同的處理器(handler)可以將日志輸出到不同的位置;
- 日志器(logger)可以設置多個處理器(handler)將同一條日志記錄輸出到不同的位置;
- 每個處理器(handler)都可以設置自己的過濾器(filter)實現日志過濾,從而只保留感興趣的日志;
- 每個處理器(handler)都可以設置自己的格式器(formatter)實現同一條日志以不同的格式輸出到不同的地方。
簡單點說就是:日志器(logger)是入口,真正干活兒的是處理器(handler),處理器(handler)還可以通過過濾器(filter)和格式器(formatter)對要輸出的日志內容做過濾和格式化等處理操作。
2.logging日志模塊相關類及其常用方法介紹
下面介紹與logging四大組件相關的類:Logger,Handler,Filter,Formatter
Logger類
Logger對象有3個任務要做:
- 1.向應用程序代碼暴露幾個方法,使應用程序可以在運行時記錄日志消息;
- 2.基于日志嚴重等級(默認的過濾設施)或filter對象來決定要對哪些日志進行后續處理;
- 3.將日志消息傳送給所有感興趣的日志handlers
Logger對象最常用的方法分為兩類:配置方法和消息發送方法
最常用的配置方法如下:
Logger.setLevel():設置日志器將會處理的日志消息的最低嚴重級別
Logger.addHandler()/Logger.removeHandler():為該logger對象添加和移除一個handler對象
Logger.addFilter()/Logger.removeFilter():為該logger對象添加和移除一個filter對象
關于Logger.setLevel()方法的說明:
內建等級中,級別最低的是DEBUG,級別最高的是CRITICAL。例如setLevel(logging.INFO),此時函數參數為INFO,那么該logger將只會處理INFO、WARNING、ERROR和CRITICAL級別的日志,而DEBUG級別的消息將會被忽略/丟棄。
logger對象配置完成后,可以使用下面的方法來創建日志記錄:
Logger.debug(), Logger.info()......Logger.critical():創建一個與它們的方法名對應等級的日志記錄
Logger.exception():創建一個類似于Logger.error()的日志消息
Logger.log():需要獲取一個明確的日志level參數來創建一個日志記錄
說明: - Logger.exception()與Logger.error()的區別在于:Logger.exception()將會輸出堆棧追蹤信息,另外通常只是在一個exception handler中調用該方法。
- Logger.log()與Logger.debug() Logger.info()等方法相比,雖然需要多傳一個level參數,顯得不是那么方便,但是當需要記錄自定義level的日志時還是需要該方法來完成。
那么,怎樣得到一個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.setLevel() 設置handler將會處理的日志消息的最低嚴重級別
Handler.setFormatter() 為handler設置一個格式器對象
Handler.addFilter() 和 Handler.removeFilter() 為handler添加 和 刪除一個過濾器對象
需要說明的是,應用程序代碼不應該直接實例化和使用Handler實例。因為Handler是一個基類,它只定義了素有handlers都應該有的接口,同時提供了一些子類可以直接使用或覆蓋的默認行為。下面是一些常用的Handler:
logging.StreamHandler 將日志消息發送到輸出到Stream,如std.out, std.err或任何file-like對象。
logging.FileHandler 將日志消息發送到磁盤文件,默認情況下文件大小會無限增長
logging.handlers.RotatingFileHandler 將日志消息發送到磁盤文件,并支持日志文件按大小切割
logging.hanlders.TimedRotatingFileHandler 將日志消息發送到磁盤文件,并支持日志文件按時間切割
logging.handlers.HTTPHandler 將日志消息以GET或POST的方式發送給一個HTTP服務器
logging.handlers.SMTPHandler 將日志消息發送給一個指定的email地址
logging.NullHandler 該Handler實例會忽略error messages,通常被想使用logging的library開發者使用來避免'No handlers could be found for logger XXX'信息的出現。