在部署項目時,不可能直接將所有的信息都輸出到控制台中,我們可以將這些信息記錄到日志文件中,這樣不僅方便我們查看程序運行時的情況,也可以在項目出現故障時根據運行時產生的日志快速定位問題出現的位置。
1、日志級別
Python 標准庫 logging 用作記錄日志,默認分為六種日志級別(括號為級別對應的數值),NOTSET(0)、DEBUG(10)、INFO(20)、WARNING(30)、ERROR(40)、CRITICAL(50)。我們自定義日志級別時注意不要和默認的日志級別數值相同,logging 執行時輸出大於等於設置的日志級別的日志信息,如設置日志級別是 INFO,則 INFO、WARNING、ERROR、CRITICAL 級別的日志都會輸出。
2、logging 流程
官方的 logging 模塊工作流程圖如下:
從下圖中我們可以看出看到這幾種 Python 類型,Logger、LogRecord、Filter、Handler、Formatter。
類型說明:
Logger:日志,暴露函數給應用程序,基於日志記錄器和過濾器級別決定哪些日志有效。
LogRecord :日志記錄器,將日志傳到相應的處理器處理。
Handler :處理器, 將(日志記錄器產生的)日志記錄發送至合適的目的地。
Filter :過濾器, 提供了更好的粒度控制,它可以決定輸出哪些日志記錄。
Formatter:格式化器, 指明了最終輸出中日志記錄的布局。
3、日志輸出格式
日志的輸出格式可以認為設置,默認格式為下圖所示。
4、基本使用
logging 使用非常簡單,使用 basicConfig() 方法就能滿足基本的使用需要,如果方法沒有傳入參數,會根據默認的配置創建Logger 對象,默認的日志級別被設置為 WARNING,默認的日志輸出格式如上圖,該函數可選的參數如下表所示。
示例代碼如下:
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')
復制代碼
輸出結果如下:
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
復制代碼
傳入常用的參數,示例代碼如下(這裡日志格式占位符中的變量放到後面介紹):
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 ,內容如下:
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
復制代碼
但是當發生異常時,直接使用無參數的 debug()、info()、warning()、error()、critical() 方法並不能記錄異常信息,需要設置 exc_info 參數為 True 才可以,或者使用 exception() 方法,還可以使用 log() 方法,但還要設置日志級別和 exc_info 參數。
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 模塊,但一般並不能滿足實際使用,我們還需要自定義 Logger。
一個系統只有一個 Logger 對象,並且該對象不能被直接實例化,沒錯,這裡用到了單例模式,獲取 Logger 對象的方法為 getLogger。
注意:這裡的單例模式並不是說只有一個 Logger 對象,而是指整個系統只有一個根 Logger 對象,Logger 對象在執行 info()、error() 等方法時實際上調用都是根 Logger 對象對應的 info()、error() 等方法。
我們可以創造多個 Logger 對象,但是真正輸出日志的是根 Logger 對象。每個 Logger 對象都可以設置一個名字,如果設置logger = logging.getLogger(__name__)
,__name__ 是 Python 中的一個特殊內置變量,他代表當前模塊的名稱(默認為 __main__)。則 Logger 對象的 name 為建議使用使用以點號作為分隔符的命名空間等級制度。
Logger 對象可以設置多個 Handler 對象和 Filter 對象,Handler 對象又可以設置 Formatter 對象。Formatter 對象用來設置具體的輸出格式,常用變量格式如下表所示,所有參數見 Python(3.7)官方文檔:
Logger 對象和 Handler 對象都可以設置級別,而默認 Logger 對象級別為 30 ,也即 WARNING,默認 Handler 對象級別為 0,也即 NOTSET。logging 模塊這樣設計是為了更好的靈活性,比如有時候我們既想在控制台中輸出DEBUG 級別的日志,又想在文件中輸出WARNING級別的日志。可以只設置一個最低級別的 Logger 對象,兩個不同級別的 Handler 對象,示例代碼如下:
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')
復制代碼
控制台輸出結果為:
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
復制代碼
文件中輸出內容為:
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
復制代碼
創建了自定義的 Logger 對象,就不要在用 logging 中的日志輸出方法了,這些方法使用的是默認配置的 Logger 對象,否則會輸出的日志信息會重復。
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')
復制代碼
輸出結果如下(可以看到日志信息被輸出了兩遍):
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
復制代碼
說明:在引入有日志輸出的 python 文件時,如 import test.py
,在滿足大於當前設置的日志級別後就會輸出導入文件中的日志。
6、Logger 配置
通過上面的例子,我們知道創建一個 Logger 對象所需的配置了,上面直接硬編碼在程序中配置對象,配置還可以從字典類型的對象和配置文件獲取。打開 logging.config Python 文件,可以看到其中的配置解析轉換函數。
從字典中獲取配置信息:
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")
# 省略日志輸出
復制代碼
從配置文件中獲取配置信息:
常見的配置文件有 ini 格式、yaml 格式、JSON 格式,或者從網絡中獲取都是可以的,只要有相應的文件解析器解析配置即可,下面只展示了 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 庫
import yaml
with open('test.yaml', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger("sampleLogger")
# 省略日志輸出
復制代碼
7、實戰中的問題
1、中文亂碼
上面的例子中日志輸出都是英文內容,發現不了將日志輸出到文件中會有中文亂碼的問題,如何解決到這個問題呢?FileHandler 創建對象時可以設置文件編碼,如果將文件編碼設置為 “utf-8”(utf-8 和 utf8 等價),就可以解決中文亂碼問題啦。一種方法是自定義 Logger 對象,需要寫很多配置,另一種方法是使用默認配置方法 basicConfig(),傳入 handlers 處理器列表對象,在其中的 handler 設置文件的編碼。網上很多都是無效的方法,關鍵參考代碼如下:
# 自定義 Logger 配置
handler = logging.FileHandler(filename="test.log", encoding="utf-8")
復制代碼
# 使用默認的 Logger 配置
logging.basicConfig(handlers=[logging.FileHandler("test.log", encoding="utf-8")], level=logging.DEBUG)
復制代碼
2、臨時禁用日志輸出
有時候我們又不想讓日志輸出,但在這後又想輸出日志。如果我們打印信息用的是 print() 方法,那麼就需要把所有的 print() 方法都注釋掉,而使用了 logging 後,我們就有了一鍵開關閉日志的 "魔法"。一種方法是在使用默認配置時,給 logging.disabled() 方法傳入禁用的日志級別,就可以禁止設置級別以下的日志輸出了,另一種方法時在自定義 Logger 時,Logger 對象的 disable 屬性設為 True,默認值是 False,也即不禁用。
logging.disable(logging.INFO)
復制代碼
logger.disabled = True
復制代碼
3、日志文件按照時間劃分或者按照大小劃分
如果將日志保存在一個文件中,那麼時間一長,或者日志一多,單個日志文件就會很大,既不利於備份,也不利於查看。我們會想到能不能按照時間或者大小對日志文件進行劃分呢?答案肯定是可以的,並且還很簡單,logging 考慮到了我們這個需求。logging.handlers 文件中提供了 TimedRotatingFileHandler 和 RotatingFileHandler 類分別可以實現按時間和大小劃分。打開這個 handles 文件,可以看到還有其他功能的 Handler 類,它們都繼承自基類 BaseRotatingHandler。
# TimedRotatingFileHandler 類構造函數
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None):
# RotatingFileHandler 類的構造函數
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)
復制代碼
示例代碼如下:
# 每隔 1000 Byte 劃分一個日志文件,備份文件為 3 個
file_handler = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8")
復制代碼
# 每隔 1小時 劃分一個日志文件,interval 是時間間隔,備份文件為 10 個
handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10)
復制代碼
Python 官網雖然說 logging 庫是線程安全的,但在多進程、多線程、多進程多線程環境中仍然還有值得考慮的問題,比如,如何將日志按照進程(或線程)劃分為不同的日志文件,也即一個進程(或線程)對應一個文件。由於本文篇幅有限,故不在這裡做詳細說明,只是起到引發讀者思考的目的,這些問題我會在另一篇文章中討論。
作者:Wizey
鏈接:https://juejin.cn/post/6844903692915703815
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。