python 日誌 logging 模組詳解
文章目錄
1 日誌相關概念
1.1 日誌的作用
- 程式除錯
- 瞭解程式執行是否正常
- 故障分析與問題定位
- 使用者行為分析
1.2 日誌的等級
等級 | 含義 |
---|---|
DEBUG | 最詳細的日誌資訊,典型應用場景是問題診斷 |
INFO | 資訊詳細程度僅次於 DEBUG,通常只記錄關鍵節點資訊,用於確認一切都是按照我們預期的那樣進行工作 |
WARNING | 當某些不期望的事情發生時記錄的資訊(如,磁碟可用空間較低),但是此時應用程式還是正常執行的 |
ERROR | 由於一個更嚴重的問題導致某些功能不能正常執行時記錄的信 |
CRITICAL | 當發生嚴重錯誤,導致應用程式不能繼續執行時記錄的資訊 |
預設情況下,logging 模組將等級為 WARNING 及其以上的日誌資訊列印到控制檯
1.3 logging 模組兩種使用方式
logging 模組有兩種使用方式
- 第一種方式是使用 logging 提供的模組級別的函式
- 第二種方式是使用 Logging 日誌系統的四大元件
2 使用 logging 提供的模組級別的函式
2.1 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 進行一次性配置 |
下面進行使用演示:
2.2 使用方式1:簡單配置
import logging
logging.debug("debug message")
logging.info("info message")
logging.warning("warning message")
logging.error("error message")
logging.critical("critical message")
logging.log(level=logging.ERROR, msg = "error in logging.log function")
輸出結果:
WARNING:root:warning message
ERROR:root:error message
CRITICAL:root:critical message
ERROR:root:error in logging.log function
預設情況下 logging 模組將日誌列印到了標準輸出中,且只顯示大於等於 WARNING 級別的日誌,這說明預設的日誌級別設定為 WARNING(日誌級別等級 CRITICAL > ERROR > WARNING > INFO > DEBUG)
2.3 使用方式2:使用 logging.basicConfig() 函式
使用 logging.basicConfig() 函式可以調整日誌級別、輸出格式等
logging.basicConfig() 函式說明
引數名 | 描述 |
---|---|
filename | 指定日誌輸出目標檔案的檔名,指定該設定項後日志資訊就不會被輸出到控制檯了 |
format | 指定日誌格式字串,即指定日誌輸出時所包含的欄位資訊以及它們的順序。logging 模組定義的格式欄位下面會列出。 |
datefmt | 指定日期/時間格式。需要注意的是,該選項要在 format 中包含時間欄位 %(asctime)s 時才有效 |
level | 指定日誌器的日誌級別 |
stream | 指定日誌輸出目標 stream,如 sys.stdout、sys.stderr 以及網路 stream。需要說明的是,stream 和 filename 不能同時提供,否則會引發 ValueError 異常 |
style | Python3.2 中新新增的配置項。指定 format 格式字串的風格,可取值為 ‘%’、’{’ 和 ‘$’,預設為 ‘%’ |
handlers | Python 3.3 中新新增的配置項。該選項如果被指定,它應該是一個建立了多個 Handler 的可迭代物件,這些 handler 將會被新增到 rootlogger。需要說明的是:filename、stream 和 handlers 這三個配置項只能有一個存在,不能同時出現 2 個或 3 個,否則會引發 ValueError 異常。 |
logging 模組的格式字串
欄位/屬性名稱 | 使用格式 | 描述 |
---|---|---|
asctime | %(asctime)s | 日誌事件發生的時間–人類可讀時間,如:2003-07-08 16:49:45,896 |
created | %(created)f | 日誌事件發生的時間–時間戳,就是當時呼叫 time.time() 函式返回的值 |
relativeCreated | %(relativeCreated)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 | 程序名稱,Python 3.1 新增 |
thread | %(thread)d | 執行緒 ID |
threadName | %(thread)s | 執行緒名稱 |
# coding=utf-8
import logging
MY_FORMAT = "%(asctime)s %(name)s %(levelname)s %(pathname)s %(lineno)d %(message)s" # 配置輸出日誌格式
DATE_FORMAT = '%Y-%m-%d %H:%M:%S %a ' #配置輸出時間的格式
logging.basicConfig(
filename="my.log", # 指定日誌寫入到檔案
level=logging.INFO,
datefmt=DATE_FORMAT,
format=MY_FORMAT,
)
logging.debug("debug")
logging.info("info")
logging.warning("warning")
logging.error("error")
logging.critical("critical")
開啟檔案 my.log,內容如下:
2020-11-22 19:18:58 Sun root INFO E:/prapy/python_project/testcase/test1.py 15 info
2020-11-22 19:18:58 Sun root WARNING E:/prapy/python_project/testcase/test1.py 16 warning
2020-11-22 19:18:58 Sun root ERROR E:/prapy/python_project/testcase/test1.py 17 error
2020-11-22 19:18:58 Sun root CRITICAL E:/prapy/python_project/testcase/test1.py 18 critical
說明:
- logging.basicConfig() 函式是一個一次性的簡單配置工具使,也就是說只有在第一次呼叫該函式時會起作用,後續再次呼叫該函式時完全不會產生任何操作的,多次呼叫的設定並不是累加操作。
- 日誌器(Logger)是有層級關係的,上面呼叫的 logging 模組級別的函式所使用的日誌器是 RootLogger 類的例項,其名稱為 ‘root’,它是處於日誌器層級關係最頂層的日誌器,且該例項是以單例模式存在的。
- 如果要記錄的日誌中包含變數資料,可使用一個格式字串作為這個事件的描述訊息(logging.debug、logging.info 等函式的第一個引數),然後將變數資料作為第二個引數 *args 的值進行傳遞,
>>> import logging
>>> logging.warning('%s is %d years old.', 'Tom', 10)
WARNING:root:Tom is 10 years old.
3 使用 Logging 日誌系統的四大元件
上面我們瞭解到了 logging.debug()、logging.info()、logging.warning()、logging.error()、logging.critical()(分別用以記錄不同級別的日誌資訊),logging.basicConfig()(用預設日誌格式(Formatter)為日誌系統建立一個預設的流處理器(StreamHandler),設定基礎配置(如日誌級別等)並加到 root logger(根 Logger)中)這幾個 logging 模組級別的函式。
下面介紹第二種列印日誌的方法,日誌流處理,使用函式 logging.getLogger([name])(返回一個 logger 物件,如果沒有指定名字將返回 root logger)。
在介紹 logging 模組的日誌流處理流程之前,我們先來介紹下 logging 模組的四大元件:
元件名稱 | 對應類名 | 功能描述 |
---|---|---|
日誌器 | Logger | 提供了應用程式可一直使用的介面 |
處理器 | Handler | 將 logger 建立的日誌記錄傳送到合適的目的輸出 |
過濾器 | Filter | 提供了更細粒度的控制工具來決定輸出哪條日誌記錄,丟棄哪條日誌記錄 |
格式器 | Formatter | 決定日誌記錄的最終輸出格式 |
這些元件之間的關係描述:
- 日誌器(logger)需要通過處理器(handler)將日誌資訊輸出到目標位置,如:檔案、sys.stdout、網路等;
- 不同的處理器(handler)可以將日誌輸出到不同的位置;
- 日誌器(logger)可以設定多個處理器(handler)將同一條日誌記錄輸出到不同的位置;
- 每個處理器(handler)都可以設定自己的過濾器(filter)實現日誌過濾,從而只保留感興趣的日誌;
- 每個處理器(handler)都可以設定自己的格式器(formatter)實現同一條日誌以不同的格式輸出到不同的地方。
簡單點說就是:日誌器(logger)是入口,真正幹活兒的是處理器(handler),處理器(handler)還可以通過過濾器(filter)和格式器(formatter)對要輸出的日誌內容做過濾和格式化等處理操作。
logging 日誌模組相關類及其常用方法介紹
與 logging 四大元件相關的類:Logger, Handler, Filter, Formatter。
3.1 Logger 類
Logger 物件有 3 個任務要做:
- 嚮應用程式程式碼暴露幾個方法,使應用程式可以在執行時記錄日誌訊息;
- 基於日誌嚴重等級(預設的過濾設施)或 filter 物件來決定要對哪些日誌進行後續處理;
- 將日誌訊息傳送給所有感興趣的日誌 handlers。
Logger 物件最常用的方法分為兩類:配置方法和訊息傳送方法
Logger 類相關方法
方法 | 描述 |
---|---|
Logger.setLevel() | 設定日誌器將會處理的日誌訊息的最低嚴重級別 |
Logger.addHandler() 和 Logger.removeHandler() | 為該logger物件新增 和 移除一個handler物件 |
Logger.addFilter() 和 Logger.removeFilter() | 為該logger物件新增 和 移除一個filter物件 |
logger物件配置完成後,可以使用下面的方法來建立日誌記錄:
方法 | 描述 |
---|---|
Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), Logger.critical() | 建立一個與它們的方法名對應等級的日誌記錄 |
Logger.exception() | 建立一個類似於 Logger.error() 的日誌訊息 |
Logger.log() | 需要獲取一個明確的日誌 level 引數來建立一個日誌記錄 |
一個 Logger 物件呢?一種方式是通過 Logger 類的例項化方法建立一個 Logger 類的例項,但是我們通常都是用第二種方式–logging.getLogger() 方法。
logging.getLogger() 方法有一個可選引數 name,該引數表示將要返回的日誌器的名稱標識,如果不提供該引數,則其值為 ‘root’。若以相同的 name 引數值多次呼叫 getLogger() 方法,將會返回指向同一個 logger 物件的引用。
多次使用注意不能建立多個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 來關閉這種傳遞機制。
3.2 Handler 類
Handler 物件的作用是(基於日誌訊息的 level)將訊息分發到 handler 指定的位置(檔案、網路、郵件等)。Logger 物件可以通過 addHandler() 方法為自己新增 0 個或者更多個 handler 物件。比如,一個應用程式可能想要實現以下幾個日誌需求:
方法 | 描述 |
---|---|
Handler.setLevel(lel) | 指定被處理的資訊級別,低於 lel 級別的資訊將被忽略 |
Handler.setFormatter() | 給這個 handler 選擇一個格式 |
Handler.addFilter(filt)、Handler.removeFilter(filt) | 新增或刪除一個 filter 物件 |
需要說明的是,應用程式程式碼不應該直接例項化和使用 Handler 例項。因為 Handler 是一個基類,它只定義了所有 handlers 都應該有的介面,同時提供了一些子類可以直接使用或覆蓋的預設行為。下面是一些常用的 Handler:
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’ 資訊的出現。 |
3.3 Formater 類
Formater 物件用於配置日誌資訊的最終順序、結構和內容。與 logging.Handler基類不同的是,應用程式碼可以直接例項化 Formatter 類。另外,如果你的應用程式需要一些特殊的處理行為,也可以實現一個 Formatter 的子類來完成。
Formatter 類的構造方法定義如下:
logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
可見,該構造方法接收 3 個可選引數:
- fmt:指定訊息格式化字串,如果不指定該引數則預設使用 message 的原始值
- datefmt:指定日期格式字串,如果不指定該引數則預設使用 “%Y-%m-%d %H:%M:%S”
- style:Python 3.2 新增的引數,可取值為 ‘%’,’{’ 和 ‘$’,如果不指定該引數則預設使用 ‘%’
一般直接用 logging.Formatter(fmt, datefmt)
3.4 Filter類(瞭解即可)
Filter 可以被 Handler 和 Logger 用來做比 level 更細粒度的、更復雜的過濾功能。Filter 是一個過濾器基類,它只允許某個 logger 層級下的日誌事件通過過濾。該類定義如下:
class logging.Filter(name='')
filter(record)
比如,一個 filter 例項化時傳遞的 name 引數值為 ‘A.B’,那麼該 filter 例項將只允許名稱為類似如下規則的 loggers 產生的日誌記錄通過濾:‘A.B’,‘A.B,C’,‘A.B.C.D’,‘A.B.D’,而名稱為 ‘A.BB’,‘B.A.B’ 的 loggers 產生的日誌則會被過濾掉。如果 name 的值為空字串,則允許所有的日誌事件通過過濾。
filter 方法用於具體控制傳遞的 record 記錄是否能通過過濾,如果該方法返回值為0表示不能通過過濾,返回值為非 0 表示可以通過過濾。
3.5 日誌流處理簡要流程
1、建立一個 logger
2、設定下 logger 的日誌的等級
3、建立合適的 Handler(FileHandler 要有路徑)
4、設定下每個 Handler 的日誌等級
5、建立下日誌的格式
6、向 Handler 中新增上面建立的格式
7、將上面建立的 Handler 新增到 logger 中
8、列印輸出 logger.debug\logger.info\logger.warning\logger.error\logger.critical
# coding=utf-8
import logging
# 建立logger,如果引數為空則返回 root logger
logger = logging.getLogger("mylogger")
logger.setLevel(logging.DEBUG) # 設定logger日誌等級
# 建立handler
fh = logging.FileHandler("test.log", encoding="utf-8")
ch = logging.StreamHandler()
# 設定輸出日誌格式, 注意 logging.Formatter的大小寫
formatter = logging.Formatter(
fmt="%(asctime)s %(name)s %(filename)s %(message)s",
datefmt="%Y/%m/%d %X"
)
# 為handler指定輸出格式,注意大小寫
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 為logger新增的日誌處理器
logger.addHandler(fh)
logger.addHandler(ch)
# 輸出不同級別的log
logger.warning("warning message")
logger.info("info message")
logger.error("error message")
執行結果
2020/11/22 21:00:24 mylogger test3.py warning message
2020/11/22 21:00:24 mylogger test3.py info message
2020/11/22 21:00:24 mylogger test3.py error message
python logging 重複寫日誌問題
用 Python 的 logging 模組記錄日誌時,可能會遇到重複記錄日誌的問題,第一條記錄寫一次,第二條記錄寫兩次,第三條記錄寫三次
原因:沒有移除 handler 解決:在日誌記錄完之後 removeHandler
# coding=utf-8
import logging
def log(msg):
#建立logger,如果引數為空則返回root logger
logger = logging.getLogger("mylogger")
logger.setLevel(logging.DEBUG) #設定logger日誌等級
#建立handler
fh = logging.FileHandler("test.log",encoding="utf-8")
ch = logging.StreamHandler()
#設定輸出日誌格式
formatter = logging.Formatter(
fmt="%(asctime)s %(name)s %(filename)s %(message)s",
datefmt="%Y/%m/%d %X"
)
#為handler指定輸出格式
fh.setFormatter(formatter)
ch.setFormatter(formatter)
#為logger新增的日誌處理器
logger.addHandler(fh)
logger.addHandler(ch)
# 輸出不同級別的log
logger.info(msg)
# 輸出不同級別的log
log("message1")
log("message2")
log("message3")
執行結果
2020/11/22 21:08:04 mylogger test3.py message1
2020/11/22 21:08:04 mylogger test3.py message2
2020/11/22 21:08:04 mylogger test3.py message2
2020/11/22 21:08:04 mylogger test3.py message3
2020/11/22 21:08:04 mylogger test3.py message3
2020/11/22 21:08:04 mylogger test3.py message3
分析:可以看到輸出結果有重複列印
原因:第二次呼叫 log 的時候,根據 getLogger(name) 裡的 name 獲取同一個logger,而這個 logger 裡已經有了第一次你新增的 handler,第二次呼叫又添加了一個 handler,所以,這個 logger 裡有了兩個同樣的 handler,以此類推,呼叫幾次就會有幾個 handler。
解決方案 1:新增 removeHandler 語句
# coding=utf-8
import logging
def log(msg):
# 建立logger,如果引數為空則返回root logger
logger = logging.getLogger("mylogger")
logger.setLevel(logging.DEBUG) # 設定logger日誌等級
# 建立handler
fh = logging.FileHandler("test.log", encoding="utf-8")
ch = logging.StreamHandler()
# 設定輸出日誌格式
formatter = logging.Formatter(
fmt="%(asctime)s %(name)s %(filename)s %(message)s",
datefmt="%Y/%m/%d %X"
)
# 為handler指定輸出格式
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 為logger新增的日誌處理器
logger.addHandler(fh)
logger.addHandler(ch)
# 輸出不同級別的log
logger.info(msg)
# 解決方案1,新增removeHandler語句,每次用完之後移除Handler
logger.removeHandler(fh)
logger.removeHandler(ch)
# 輸出不同級別的log
log("message1")
log("message2")
log("message3")
解決方案 2:在 log 方法裡做判斷,如果這個 logger 已有 handler,則不再新增 handler。
# coding=utf-8
import logging
def log(msg):
# 建立logger,如果引數為空則返回root logger
logger = logging.getLogger("mylogger")
logger.setLevel(logging.DEBUG) # 設定logger日誌等級
if not logger.handlers:
# 建立handler
fh = logging.FileHandler("test.log", encoding="utf-8")
ch = logging.StreamHandler()
# 設定輸出日誌格式
formatter = logging.Formatter(
fmt="%(asctime)s %(name)s %(filename)s %(message)s",
datefmt="%Y/%m/%d %X"
)
# 為handler指定輸出格式
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 為logger新增的日誌處理器
logger.addHandler(fh)
logger.addHandler(ch)
# 輸出不同級別的log
logger.info(msg)
# 輸出不同級別的log
log("message1")
log("message2")
log("message3")
logger 呼叫方法的例子
# coding=utf-8
import logging.handlers
import datetime
def get_logger():
logger = logging.getLogger('mylogger') # mylogger為日誌器的名稱標識,如果不提供該引數,預設為'root'
logger.setLevel(logging.DEBUG) # 設定logger處理等級
# 這裡進行判斷,如果logger.handlers列表為空,則新增,否則,直接去寫日誌
if not logger.handlers:
# rf_handler將所有的日誌資訊寫到 all.log 中
# when:字串,定義了日誌切分的間隔時間單位
# interval:間隔時間單位的個數,指等待多少個when的時間後Logger會自動重建新聞繼續進行日誌記錄
# backupCount 是保留日誌的檔案個數,日誌檔案最多backupCount個,多餘的刪除,預設為0,表示不會自動刪除
rf_handler = logging.handlers.TimedRotatingFileHandler('all.log', when='midnight', interval=1, backupCount=7,
atTime=datetime.time(0, 0, 0, 0))
# 設定輸出日誌格式
rf_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
# 為handler指定輸出格式
rf_handler.setFormatter(rf_formatter)
# f_handler 將等級大於等於 error的資訊寫到error.log檔案中
f_handler = logging.FileHandler('error.log')
f_handler.setLevel(logging.ERROR)
# 設定輸出日誌格式
f_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s")
# 為handler指定輸出格式
f_handler.setFormatter(f_formatter)
# 為logger新增的日誌處理器
logger.addHandler(rf_handler)
logger.addHandler(f_handler)
return logger
logger = get_logger()
logger.debug('debug message')
logger.info('info message')
logger.warning('warning message')
logger.error('error message')
logger.critical('critical message')
logger.log(level=logging.ERROR, msg="logger.log message")
參考:https://www.cnblogs.com/Nicholas0707/p/9021672.html#_label1_1