python模塊之logging
一 日誌記錄的重要性
? 在開發過程中,如果程序運行出現了問題,通常可以經過debug來檢測到底是哪一步出現了問題,如果出現了問題的話,是比較容易排查出問題的。但是程序開發完成之後,部署到生產環境中去,這時候代碼相當於是在一個黑盒環境下運行的,我們只能看到其運行的效果,是不能直接看到代碼運行過程中的每一步狀態的。在生產環境既不能通過debug來排查問題,更不能將生產環境下線來排查問題,所以這時候記錄日誌就很重要了。
? 如果我們現在只能得知當前問題的現象,而沒有其他任何信息的話,如果想要解決掉這個問題的話,只能通過問題出現的形式來嘗試重現bug,然後在一步一步的調試,這恐怕是難的,這樣很大的概率是無法精準的復現這個問題的,而且debug的過程中也會耗費巨多的時間,這樣如果在生產環境上出現了問題的話,修復bug就會變得非常棘手。但是如果有日誌記錄的話,不論是正常運行還是報錯,都有相關的時間記錄、狀態記錄、錯誤記錄等,那麽這樣我們就可以方便的追蹤到在當時的運行過程中出現了怎樣的狀況,從而可以快速的排查問題。
? 因此,作為開發者記錄生產環境中代碼運行的日誌是很有必要的,因此學好怎麽記錄日誌過程是一門必修課。
二 日誌記錄的流程框架
在python中,怎樣才能算做一個比較標準的日誌記錄呢?像我之前都是使用控制臺查看結果或者debug查看運行狀態的,對於一個小項目來說,debug已經足夠了,但是對於一個大項目來說一步一步調試費時費力,而且得到的消息也不一定全面,如果自己可以定義輸出的調試信息那麽對於調試來講是件很節省時間的事。
在python中專門有一個用來記錄日誌的模塊logging,可以用它來進行標註的日誌記錄,利用它我們可以更方便的進行日誌記錄,同時還可以做更方便的級別區分以及一些額外的日誌信息的記錄,如時間、運行模塊信息等。
那麽完整的日誌記錄流程框架是什麽樣呢?
如圖所示,整個日誌記錄的框架可以分為這麽幾個部分:
- Logger:即Logger Main Class,是我們進行日誌記錄是創建的對象,可以調用Logger的方法傳入日誌模板和信息,來生成一條條日誌記錄,稱作Log Record。
- Log Record:就代指生成的一條條日誌記錄。
- Handler:即用來處理日誌記錄的類,它可以將Log Record輸出到我們指定的日誌位置和存儲形式等,如我們可以指定輸入到具體的文件、或者可以指定將日誌通過FTP協議記錄到遠程的服務器上,Handler就會幫我們完成這些事情。
- Formatter:實際上生成的Log Record也是一個個對象,那麽我們想要把它們保存成一條條我們想要的日誌文本的話,就需要有一個格式化的過程,那麽這個過程就有Formatter來完成,返回的就是日誌字符串,然後傳回給Handler來處理。
- Filter:另外保存日誌的時候我們可能不需要全部保存,只需要保存我們想要的部分就可以了,所以在保存的時候需要進一步過濾,留下想要的日誌信息,如只保存某個級別的日誌,那麽這個過濾過程就可以交給Filter來完成。
- Parent Handler:Handler之間可以存在分層關系,以使得不同Handler之間共享想通功能的代碼。
這些就是整個logging模塊的基本架構和對象功能。
三 日誌記錄的相關用法
logging模塊有如下幾個優點:
- 可以在logging模塊中設置日誌等級,在不同的版本(如開發環境、生產環境)上通過設置不同的輸出等級來記錄相應的日誌。
- logging模塊不僅可以把輸出信息輸出到控制臺,還可以設置輸出到任意位置,如寫入文件、寫入遠程服務器等。
- logging模塊具有靈活的配置和格式化功能,如配置輸出當前模塊信息、運行時間等,相比print的字符串格式化更加方便易用。
四 日誌記錄的相關用法
一 日誌級別
日誌級別分為五個級別:
等級 | 數值 |
---|---|
CRITICAL | 50 |
FATAL | 50 |
ERROR | 40 |
WARNING | 30 |
WARN | 30 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
這裏最高的等級是 CRITICAL 和 FATAL,兩個對應的數值都是 50,另外對於 WARNING 還提供了簡寫形式 WARN,兩個對應的數值都是 30。
我們設置了輸出 level,系統便只會輸出 level 數值大於或等於該 level 的的日誌結果,例如我們設置了輸出日誌 level 為 INFO,那麽輸出級別大於等於 INFO 的日誌,如 WARNING、ERROR 等,DEBUG 和 NOSET 級別的不會輸出。
在logging模塊中有對應的常量用來標識級別,默認情況下,默認的log級別是warning(30),默認輸出到控制臺。
二 自定義日誌的配置
# test.py
import logging
logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(name)s - %(levelname)s - %(message)s‘)
logger = logging.getLogger(__name__)
logger.info(‘log info‘)
logger.debug(‘log debug‘)
logger.warning(‘log warning‘)
logger.critical(‘log critical‘)
在這裏首先導入logging模塊,然後進行了一下基本的配置,通過basicConfig配置了level級別和format格式化信息,level配置為INFO級別,只輸出大於等於INFO級別的信息,format格式的字符串,包括asctime、name、levelname、message四個內容,分別代表運行時間、模塊名稱、日誌級別、日誌內容,這樣輸出內容便為這四個內容的信息了,這就是logging全局配置。
接下來聲明了一個Logger對象,是日誌輸出的主類,調用對象的info()方法可以輸出INFO級別的日誌信息,調用debug()方法就可以輸出DEBUG級別的日誌信息,其他級別的日誌信息同理可以輸出。初始化logger時傳入了__name__來代替傳入模塊的名稱,如果直接運行該腳本則值為__main__,如果作為模塊被import的話,則就是被導入模塊的名稱為test.py,因為在不同的模塊中該值不同,所以直接使用__name__代替,然後輸出了四條日誌信息,其中有一條INFO信息,一條DEBUG信息,一條WARNING信息,一條CRITICAL信息,因為設置的level為INFO,所以低於INFO的級別日誌會被過濾,所以DEBUG日誌信息會被過濾。
三 basicConfig的具體參數
basicConfig是用作全局的日誌配置,basicConfig的參數有:
- filename:日誌輸出的文件名,如果指定了這個信息之後,不會使用默認的StreamHandler,會使用FileHandler來將日誌信息輸入到指定的文件中。
- filemode:該參數為指定日誌文件的寫入方式,有兩種形式,一種為w,一種為a,分別代表覆蓋寫入和追加寫入。
- format:指定日誌信息的輸出格式,即上文示例所示的參數,部分參數如下:
- %(levelno)s:打印日誌級別的值;
- %(levelname)s:打印日誌級別的名稱;
- %(pathname)s:打印當前執行程序的路徑,其實就是sys.argv[0];
- %(filenam)s:打印當前執行程序名;
- %(funcName)s:打印日誌的當前函數;
- %(lineno)s:打印日誌的當前行號;
- %(asctime)s:打印日誌的時間;
- %(thread)s:打印線程id;
- %(threadName)s:打印線程名稱;
- %(process)s:打印進程id;
- %(processName)s:打印進程名稱;
- %(module)s:打印模塊名稱;
- %(message)s:打印日誌信息。
- datefmt:指定時間的輸出格式。
- style:如果format參數制定了,這個參數就可以指定格式化時的占位符風格,如%、{、$等。
- level:指定日誌輸出的類別,程序會輸出大於等於此級別的信息。
- stream:在沒有指定filename的時候會默認使用StreamHandler,這時stream可以指定初始化的文件流。
- handlers:可以指定日誌處理時所使用的Handlers,必須是可叠代的。
四 Formatter配置
在進行日誌格式化輸出的時候,我們可以不借助於basicConfig來全局配置格式化輸出內容,可以借助Formatter來完成
Formatter用法:
import logging
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARN)
formatter = logging.Formatter(fmt=‘%(asctime)s - %(name)s - %(levelname)s - %(message)s‘, datefmt=‘%Y/%m/%d %H:%M:%S‘)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
#log
logger.debug(‘debug‘)
logger.critical(‘critical‘)
logger.error(‘error‘)
logger.warning(‘warning‘)
logger.info(‘info‘)
在這裏指定了一個Formatter,並傳入fmt和datefmt參數,這樣就指定了日誌結果的輸出格式和時間格式,然後Handler通過setFormatter()方法設置此Formatter對象即可,輸出結果如下:
2018/10/20 00:26:25 - __main__ - CRITICAL - critical
2018/10/20 00:26:25 - __main__ - ERROR - error
2018/10/20 00:26:25 - __main__ - WARNING - warning
這樣可以為每個Handler單獨配置輸出的格式。
五 捕獲Tracback異常信息
遇到錯誤的時候,希望報錯時出現詳細的Tracback信息,便於我們調試,利用logging模塊可以非常方便的實現這種需求。
import logging
logger = logging.getLogger(__name__)
logger.serLevel(level=logging.DEBUG)
formatter = logging.Formatter(‘%(asctime)s - %(name)s - %(levelname)s - %(message)s‘)
file_handler = logging.FileHandler(‘test.log‘)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
logger.info(‘INFO‘)
logger.warning(‘WARNING‘)
try:
num = 1/0
except Exception:
logger.error(‘something wrong‘, exc_info=True)
logger.info(‘end‘)
這裏在error()方法中添加了一個參數,將exc_info設置為True,這樣就可以輸出執行過程中的信息了,即可以輸出完整的Tracback信息。
運行結果如下:
2018-10-20 00:36:38,007 - __main__ - INFO - INFO
2018-10-20 00:36:38,007 - __main__ - WARNING - WARNING
2018-10-20 00:36:38,007 - __main__ - ERROR - something wrong
Traceback (most recent call last):
File "/Users/jingxing/PycharmProjects/python全棧/day22/log_test.py", line 58, in <module>
num = 1 / 0
ZeroDivisionError: division by zero
2018-10-20 00:36:38,007 - __main__ - INFO - end
這樣可以詳細的記錄報錯的信息,一旦出現了錯誤,可以及時的定位到出現問題的代碼。
六 配置共享
在寫項目的時候,肯定會有許多的配置信息,如果每個文件都來配置logging的話非常麻煩,而且幾乎很多地方的配置幾乎差不多的,因為此原因,logging模塊提供了父子模塊共享配置的機制,會根據Logger的名稱來自動加載父模塊的配置。
定義一個main.py文件:
# main.py
import logging
import core
logger = logging.getLogger(‘main‘)
logger.setLevel(level=logging.DEBUG)
handler = logging.FileHandler(‘test.log‘)
handler.setLevel(logging.INFO)
formatter = logging.Formatter(‘%(asctime)s - %(name)s - %(levelname)s - % (message)s‘)
handler.setFormatter(formatter)
logger.addHandler(handler)
logging.info(‘main info‘)
logging.debug(‘main debug‘)
logging.error(‘main error‘)
core.run()
在執行文件配置了日誌的輸出格式和文件路徑,同時定義了Logger的名稱為main,然後引入了另外一個模塊core,最後調用了core的run()方法。
接下來定義core.py,內容如下:
import logging
logger = logging.getLogger(‘main.core‘)
def run():
logger.info(‘core info‘)
logger.debug(‘core debug‘)
logger.error(‘core error‘)
在core.py文件裏定義了Logger的名稱為main.core,因為之前在main.py文件裏定義的Logger名稱為main,所以core.py裏面的Logger就會復用main.py裏面的Logger配置,而不用重新為core配置日誌了。
運行之後test.log結果如下:
2018-10-20 10:15:37,575 - main - INFO
2018-10-20 10:15:37,575 - main - DEBUG
2018-10-20 10:15:37,575 - main - ERROR
2018-10-20 10:15:37,575 - main.core - INFO
2018-10-20 10:15:37,575 - main.core - DEBUG
2018-10-20 10:15:37,575 - main.core - ERROR
可以看出main和core模塊都是用了同樣的輸出配置。所以只要在入口文件裏定義好logging模塊的輸出配置,子模塊只需要在定義Logger對象時使用父模塊的名稱開頭就可以共享配置,實際上直接使用父模塊的名字就可以,但是為了增加辨識度,可以在父模塊名字後加上後綴。
查看getLogger源碼可得,後面的名字可以自定義,然後getLogger裏面會自動幫你創建以該名字命名的日誌生成器,具體解釋如下:
獲取具有指定名稱(通道名稱)的記錄器,如果它尚不存在則創建它。 此名稱是以點分隔的分層名稱,例如“a”,“a.b”,“a.b.c”或類似名稱。如果指定名稱存在PlaceHolder [即 loggerdid不存在,但它的子節點已經存在],用createdlogger替換它,並修復指向占位符的父/子引用,現在指向記錄器。
七 文件配置
雖然可以再入口文件中定義好配置信息,然後子模塊也可以使用很方便,但是因為配置文件大部分都是不需要更改的,只是某些需要更改,像這種信息可以把配置寫入配置文件裏,然後運行時讀取配置文件裏面的配置,這樣更易維護和使用。
項目結構如下:
# bin/start.py
import os,sys
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import core.shop
def run():
print("welcome to here")
core.shop.shopping()
run()
# conf/settings
standard_format = "%(name)s %(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)s %(message)s"
simple_format = "%(name)s %(asctime)s %(module)s %(message)s"
complete_format = "%(asctime)s %(levelname)s %(funcName)s %(lineno)s %(thread)s %(process)s %(message)s"
logfile_path = r"/Users/jingxing/PycharmProjects/python全棧/day23/log/d.log"
LOGGING_DIC = {
‘version‘: 1,
‘formatters‘: {
‘standard‘: {
‘format‘: standard_format
},
‘simple‘: {
‘format‘: simple_format
},
"complete":{
"format": complete_format
}
},
‘filters‘: {},
‘handlers‘: {
‘console‘: {
‘level‘: ‘DEBUG‘,
‘class‘: ‘logging.StreamHandler‘,
‘formatter‘: ‘simple‘
},
‘default‘: {
‘level‘: ‘DEBUG‘,
‘class‘: ‘logging.handlers.RotatingFileHandler‘,
‘formatter‘: ‘standard‘,
‘filename‘: logfile_path, #
‘maxBytes‘: 1024 * 1024 * 5, # 日誌文件的最大大小為5M 超出後 換文件
‘backupCount‘: 5, # 最多留五個日誌文件
‘encoding‘: ‘utf-8‘,
},
},
‘loggers‘: {
# 在getLogger的時候 如果指定的名稱 不存在 或者不給名稱 用的就是默認的
# 在這裏如果key為空 它就是默認的
# 你可以自己定義生成器的名稱 並且他們還能使用相同的默認配置
‘‘: {
‘handlers‘: [‘default‘, ‘console‘],
‘level‘: ‘DEBUG‘,
‘propagate‘: False,
},
},
}
# core/shop.py
import lib.common
logger = lib.common.get_logger()
def shopping():
print("開始購物")
logger.debug("購物成功")
# lib/common.py
import conf.settings
import logging.config
def get_logger():
logging.config.dictConfig(conf.settings.LOGGING_DIC)
return logging.getLogger("main")
在代碼運行後,在shop.py模塊中導入了lib.common模塊,在common.py模塊中導入了conf.settings模塊,來獲取具體的日誌配置,因為getLogger(‘main‘)裏面的main在settings裏面沒有,所以使用默認的日誌配置,如下:
‘loggers‘: {
# 在getLogger的時候 如果指定的名稱 不存在 或者不給名稱 用的就是默認的
# 在這裏如果key為空 它就是默認的
# 你可以自己定義生成器的名稱 並且他們還能使用相同的默認配置
‘‘: {
‘handlers‘: [‘default‘, ‘console‘],
‘level‘: ‘DEBUG‘,
‘propagate‘: False,
}
然後首先進行level=‘DEBUG‘的過濾,因為shopping中的日誌信息級別為debug,所以會通過進入handlers裏面進行第二次過濾,在handlers裏面有兩個配置,一個為default配置,一個味console配置,在default配置中,具體配置如下:
‘default‘: {
‘level‘: ‘DEBUG‘,
‘class‘: ‘logging.handlers.RotatingFileHandler‘,
‘formatter‘: ‘standard‘,
‘filename‘: logfile_path, #
‘maxBytes‘: 1024 * 1024 * 5, # 日誌文件的最大大小為5M 超出後 換文件
‘backupCount‘: 5, # 最多留五個日誌文件
‘encoding‘: ‘utf-8‘,
}
發現日誌級別為DEBUG,所以仍然不會過濾,接下來就是格式化輸出到指定文件了,formatter為standard模式,filename之前都已經定義好了,同理console同理輸出到控制臺。
python模塊之logging