在部署項(xiàng)目時(shí),不可能直接將所有的信息都輸出到控制臺(tái)中,我們可以將這些信息記錄到日志文件中,這樣不僅方便我們查看程序運(yùn)行時(shí)的情況,也可以在項(xiàng)目出現(xiàn)故障時(shí)根據(jù)運(yùn)行時(shí)產(chǎn)生的日志快速定位問(wèn)題出現(xiàn)的位置。 1、日志級(jí)別Python 標(biāo)準(zhǔn)庫(kù) logging 用作記錄日志,默認(rèn)分為六種日志級(jí)別(括號(hào)為級(jí)別對(duì)應(yīng)的數(shù)值),NOTSET(0)、DEBUG(10)、INFO(20)、WARNING(30)、ERROR(40)、CRITICAL(50)。我們自定義日志級(jí)別時(shí)注意不要和默認(rèn)的日志級(jí)別數(shù)值相同,logging 執(zhí)行時(shí)輸出大于等于設(shè)置的日志級(jí)別的日志信息,如設(shè)置日志級(jí)別是 INFO,則 INFO、WARNING、ERROR、CRITICAL 級(jí)別的日志都會(huì)輸出。 2、logging 流程官方的 logging 模塊工作流程圖如下: 從下圖中我們可以看出看到這幾種 Python 類型,Logger、LogRecord、Filter、Handler、Formatter。 類型說(shuō)明: Logger:日志,暴露函數(shù)給應(yīng)用程序,基于日志記錄器和過(guò)濾器級(jí)別決定哪些日志有效。 LogRecord :日志記錄器,將日志傳到相應(yīng)的處理器處理。 Handler :處理器, 將(日志記錄器產(chǎn)生的)日志記錄發(fā)送至合適的目的地。 Filter :過(guò)濾器, 提供了更好的粒度控制,它可以決定輸出哪些日志記錄。 Formatter:格式化器, 指明了最終輸出中日志記錄的布局。 logging流程圖.png
3、日志輸出格式日志的輸出格式可以認(rèn)為設(shè)置,默認(rèn)格式為下圖所示。 默認(rèn)日志輸出格式.png 4、基本使用logging 使用非常簡(jiǎn)單,使用 basicConfig() 方法就能滿足基本的使用需要,如果方法沒(méi)有傳入?yún)?shù),會(huì)根據(jù)默認(rèn)的配置創(chuàng)建Logger 對(duì)象,默認(rèn)的日志級(jí)別被設(shè)置為 WARNING,默認(rèn)的日志輸出格式如上圖,該函數(shù)可選的參數(shù)如下表所示。
示例代碼如下: import logging logging.basicConfig() logging.debug('This is a debug message') logging.info('This is an info message') logging.warning('This is a warning message') logging.error('This is an error message') logging.critical('This is a critical message') 輸出結(jié)果如下: WARNING:root:This is a warning message ERROR:root:This is an error message CRITICAL:root:This is a critical message 傳入常用的參數(shù),示例代碼如下(這里日志格式占位符中的變量放到后面介紹): import logging logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG) logging.debug('This is a debug message') logging.info('This is an info message') logging.warning('This is a warning message') logging.error('This is an error message') logging.critical('This is a critical message') 生成的日志文件 test.log ,內(nèi)容如下: 13-10-18 21:10:32 root:DEBUG:This is a debug message 13-10-18 21:10:32 root:INFO:This is an info message 13-10-18 21:10:32 root:WARNING:This is a warning message 13-10-18 21:10:32 root:ERROR:This is an error message 13-10-18 21:10:32 root:CRITICAL:This is a critical message 但是當(dāng)發(fā)生異常時(shí),直接使用無(wú)參數(shù)的 debug()、info()、warning()、error()、critical() 方法并不能記錄異常信息,需要設(shè)置 exc_info 參數(shù)為 True 才可以,或者使用 exception() 方法,還可以使用 log() 方法,但還要設(shè)置日志級(jí)別和 exc_info 參數(shù)。 import logging logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG) a = 5 b = 0 try: c = a / b except Exception as e: # 下面三種方式三選一,推薦使用第一種 logging.exception("Exception occurred") logging.error("Exception occurred", exc_info=True) logging.log(level=logging.DEBUG, msg="Exception occurred", exc_info=True) 5、自定義 Logger上面的基本使用可以讓我們快速上手 logging 模塊,但一般并不能滿足實(shí)際使用,我們還需要自定義 Logger。 一個(gè)系統(tǒng)只有一個(gè) Logger 對(duì)象,并且該對(duì)象不能被直接實(shí)例化,沒(méi)錯(cuò),這里用到了單例模式,獲取 Logger 對(duì)象的方法為 getLogger。 注意:這里的單例模式并不是說(shuō)只有一個(gè) Logger 對(duì)象,而是指整個(gè)系統(tǒng)只有一個(gè)根 Logger 對(duì)象,Logger 對(duì)象在執(zhí)行 info()、error() 等方法時(shí)實(shí)際上調(diào)用都是根 Logger 對(duì)象對(duì)應(yīng)的 info()、error() 等方法。 我們可以創(chuàng)造多個(gè) Logger 對(duì)象,但是真正輸出日志的是根 Logger 對(duì)象。每個(gè) Logger 對(duì)象都可以設(shè)置一個(gè)名字,如果設(shè)置 Logger 對(duì)象可以設(shè)置多個(gè) Handler 對(duì)象和 Filter 對(duì)象,Handler 對(duì)象又可以設(shè)置 Formatter 對(duì)象。Formatter 對(duì)象用來(lái)設(shè)置具體的輸出格式,常用變量格式如下表所示,所有參數(shù)見(jiàn) Python(3.7)官方文檔:
Logger 對(duì)象和 Handler 對(duì)象都可以設(shè)置級(jí)別,而默認(rèn) Logger 對(duì)象級(jí)別為 30 ,也即 WARNING,默認(rèn) Handler 對(duì)象級(jí)別為 0,也即 NOTSET。logging 模塊這樣設(shè)計(jì)是為了更好的靈活性,比如有時(shí)候我們既想在控制臺(tái)中輸出DEBUG 級(jí)別的日志,又想在文件中輸出WARNING級(jí)別的日志。可以只設(shè)置一個(gè)最低級(jí)別的 Logger 對(duì)象,兩個(gè)不同級(jí)別的 Handler 對(duì)象,示例代碼如下: import logging import logging.handlers logger = logging.getLogger("logger") handler1 = logging.StreamHandler() handler2 = logging.FileHandler(filename="test.log") logger.setLevel(logging.DEBUG) handler1.setLevel(logging.WARNING) handler2.setLevel(logging.DEBUG) formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") handler1.setFormatter(formatter) handler2.setFormatter(formatter) logger.addHandler(handler1) logger.addHandler(handler2) # 分別為 10、30、30 # print(handler1.level) # print(handler2.level) # print(logger.level) logger.debug('This is a customer debug message') logger.info('This is an customer info message') logger.warning('This is a customer warning message') logger.error('This is an customer error message') logger.critical('This is a customer critical message') 控制臺(tái)輸出結(jié)果為: 2018-10-13 23:24:57,832 logger WARNING This is a customer warning message 2018-10-13 23:24:57,832 logger ERROR This is an customer error message 2018-10-13 23:24:57,832 logger CRITICAL This is a customer critical message 文件中輸出內(nèi)容為: 2018-10-13 23:44:59,817 logger DEBUG This is a customer debug message 2018-10-13 23:44:59,817 logger INFO This is an customer info message 2018-10-13 23:44:59,817 logger WARNING This is a customer warning message 2018-10-13 23:44:59,817 logger ERROR This is an customer error message 2018-10-13 23:44:59,817 logger CRITICAL This is a customer critical message 創(chuàng)建了自定義的 Logger 對(duì)象,就不要在用 logging 中的日志輸出方法了,這些方法使用的是默認(rèn)配置的 Logger 對(duì)象,否則會(huì)輸出的日志信息會(huì)重復(fù)。 import logging import logging.handlers logger = logging.getLogger("logger") handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) logger.debug('This is a customer debug message') logging.info('This is an customer info message') logger.warning('This is a customer warning message') logger.error('This is an customer error message') logger.critical('This is a customer critical message') 輸出結(jié)果如下(可以看到日志信息被輸出了兩遍): 2018-10-13 22:21:35,873 logger WARNING This is a customer warning message WARNING:logger:This is a customer warning message 2018-10-13 22:21:35,873 logger ERROR This is an customer error message ERROR:logger:This is an customer error message 2018-10-13 22:21:35,873 logger CRITICAL This is a customer critical message CRITICAL:logger:This is a customer critical message 說(shuō)明:在引入有日志輸出的 python 文件時(shí),如 6、Logger 配置通過(guò)上面的例子,我們知道創(chuàng)建一個(gè) Logger 對(duì)象所需的配置了,上面直接硬編碼在程序中配置對(duì)象,配置還可以從字典類型的對(duì)象和配置文件獲取。打開 logging.config Python 文件,可以看到其中的配置解析轉(zhuǎn)換函數(shù)。 從字典中獲取配置信息: import logging.config config = { 'version': 1, 'formatters': { 'simple': { 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', }, # 其他的 formatter }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'DEBUG', 'formatter': 'simple' }, 'file': { 'class': 'logging.FileHandler', 'filename': 'logging.log', 'level': 'DEBUG', 'formatter': 'simple' }, # 其他的 handler }, 'loggers':{ 'StreamLogger': { 'handlers': ['console'], 'level': 'DEBUG', }, 'FileLogger': { # 既有 console Handler,還有 file Handler 'handlers': ['console', 'file'], 'level': 'DEBUG', }, # 其他的 Logger } } logging.config.dictConfig(config) StreamLogger = logging.getLogger("StreamLogger") FileLogger = logging.getLogger("FileLogger") # 省略日志輸出 從配置文件中獲取配置信息: 常見(jiàn)的配置文件有 ini 格式、yaml 格式、JSON 格式,或者從網(wǎng)絡(luò)中獲取都是可以的,只要有相應(yīng)的文件解析器解析配置即可,下面只展示了 ini 格式和 yaml 格式的配置。 test.ini 文件 [loggers] keys=root,sampleLogger [handlers] keys=consoleHandler [formatters] keys=sampleFormatter [logger_root] level=DEBUG handlers=consoleHandler [logger_sampleLogger] level=DEBUG handlers=consoleHandler qualname=sampleLogger propagate=0 [handler_consoleHandler] class=StreamHandler level=DEBUG formatter=sampleFormatter args=(sys.stdout,) [formatter_sampleFormatter] format=%(asctime)s - %(name)s - %(levelname)s - %(message)s testinit.py 文件 import logging.config logging.config.fileConfig(fname='test.ini', disable_existing_loggers=False) logger = logging.getLogger("sampleLogger") # 省略日志輸出 test.yaml 文件 version: 1 formatters: simple: format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' handlers: console: class: logging.StreamHandler level: DEBUG formatter: simple loggers: simpleExample: level: DEBUG handlers: [console] propagate: no root: level: DEBUG handlers: [console] testyaml.py 文件 import logging.config # 需要安裝 pyymal 庫(kù) import yaml with open('test.yaml', 'r') as f: config = yaml.safe_load(f.read()) logging.config.dictConfig(config) logger = logging.getLogger("sampleLogger") # 省略日志輸出 7、實(shí)戰(zhàn)中的問(wèn)題1、中文亂碼 上面的例子中日志輸出都是英文內(nèi)容,發(fā)現(xiàn)不了將日志輸出到文件中會(huì)有中文亂碼的問(wèn)題,如何解決到這個(gè)問(wèn)題呢?FileHandler 創(chuàng)建對(duì)象時(shí)可以設(shè)置文件編碼,如果將文件編碼設(shè)置為 “utf-8”(utf-8 和 utf8 等價(jià)),就可以解決中文亂碼問(wèn)題啦。一種方法是自定義 Logger 對(duì)象,需要寫很多配置,另一種方法是使用默認(rèn)配置方法 basicConfig(),傳入 handlers 處理器列表對(duì)象,在其中的 handler 設(shè)置文件的編碼。網(wǎng)上很多都是無(wú)效的方法,關(guān)鍵參考代碼如下: # 自定義 Logger 配置 handler = logging.FileHandler(filename="test.log", encoding="utf-8") # 使用默認(rèn)的 Logger 配置 logging.basicConfig(handlers=[logging.FileHandler("test.log", encoding="utf-8")], level=logging.DEBUG) 2、臨時(shí)禁用日志輸出 有時(shí)候我們又不想讓日志輸出,但在這后又想輸出日志。如果我們打印信息用的是 print() 方法,那么就需要把所有的 print() 方法都注釋掉,而使用了 logging 后,我們就有了一鍵開關(guān)閉日志的 "魔法"。一種方法是在使用默認(rèn)配置時(shí),給 logging.disabled() 方法傳入禁用的日志級(jí)別,就可以禁止設(shè)置級(jí)別以下的日志輸出了,另一種方法時(shí)在自定義 Logger 時(shí),Logger 對(duì)象的 disable 屬性設(shè)為 True,默認(rèn)值是 False,也即不禁用。 logging.disable(logging.INFO) logger.disabled = True 3、日志文件按照時(shí)間劃分或者按照大小劃分 如果將日志保存在一個(gè)文件中,那么時(shí)間一長(zhǎng),或者日志一多,單個(gè)日志文件就會(huì)很大,既不利于備份,也不利于查看。我們會(huì)想到能不能按照時(shí)間或者大小對(duì)日志文件進(jìn)行劃分呢?答案肯定是可以的,并且還很簡(jiǎn)單,logging 考慮到了我們這個(gè)需求。logging.handlers 文件中提供了 TimedRotatingFileHandler 和 RotatingFileHandler 類分別可以實(shí)現(xiàn)按時(shí)間和大小劃分。打開這個(gè) handles 文件,可以看到還有其他功能的 Handler 類,它們都繼承自基類 BaseRotatingHandler。 # TimedRotatingFileHandler 類構(gòu)造函數(shù) def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None): # RotatingFileHandler 類的構(gòu)造函數(shù) def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False) 示例代碼如下: # 每隔 1000 Byte 劃分一個(gè)日志文件,備份文件為 3 個(gè) file_handler = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8") # 每隔 1小時(shí) 劃分一個(gè)日志文件,interval 是時(shí)間間隔,備份文件為 10 個(gè) handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10) Python 官網(wǎng)雖然說(shuō) logging 庫(kù)是線程安全的,但在多進(jìn)程、多線程、多進(jìn)程多線程環(huán)境中仍然還有值得考慮的問(wèn)題,比如,如何將日志按照進(jìn)程(或線程)劃分為不同的日志文件,也即一個(gè)進(jìn)程(或線程)對(duì)應(yīng)一個(gè)文件。由于本文篇幅有限,故不在這里做詳細(xì)說(shuō)明,只是起到引發(fā)讀者思考的目的,這些問(wèn)題我會(huì)在另一篇文章中討論。 總結(jié):Python logging 庫(kù)設(shè)計(jì)的真的非常靈活,如果有特殊的需要還可以在這個(gè)基礎(chǔ)的 logging 庫(kù)上進(jìn)行改進(jìn),創(chuàng)建新的 Handler 類解決實(shí)際開發(fā)中的問(wèn)題。 |
|