1. 程式人生 > 實用技巧 >python 日誌 logging 模組詳解

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 異常
stylePython3.2 中新新增的配置項。指定 format 格式字串的風格,可取值為 ‘%’、’{’ 和 ‘$’,預設為 ‘%’
handlersPython 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)spathname 的檔名部分,包含檔案字尾
module%(module)sfilename 的名稱部分,不包含字尾
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 個任務要做:

  1. 嚮應用程式程式碼暴露幾個方法,使應用程式可以在執行時記錄日誌訊息;
  2. 基於日誌嚴重等級(預設的過濾設施)或 filter 物件來決定要對哪些日誌進行後續處理;
  3. 將日誌訊息傳送給所有感興趣的日誌 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