Python Note 05

Python Note 05

Python 日誌利器- Loguru

在開發程式時,記錄日誌是至關重要的工作,尤其在錯誤排查、效能優化、以及應用程式監控等方面。然而標準的Python logging有時會顯得過於繁瑣,需要寫許多配置才能滿足需求。

為了簡化這些工作,Loguru 提供了豐富的功能,如格式化日誌、自動輪替檔案、日誌等級控制等,這些功能對於日常開發和運維都十分實用。

開箱即用Loguru

可以直接透過pip安裝Loguru :

pip install loguru

安裝完後就可以直接使用Loguru,無需額外的設定即可開始記錄日誌,一行代碼即可記錄日誌,Loguru預設會將日誌輸出到Terminal:

from loguru import logger

logger.info('This is info information')

並自帶格式化顯示:

2024-09-27 20:49:19.961 | INFO     | __main__:<module>:3 - This is info information

直觀且簡潔的API

前面有講過Loguru默認會將日誌輸出到Terminal,如果要取消預設設定則需要透過logger.remove來移除,handler_id=None代表刪除所有的logger:

logger.remove(handler_id=None)

如果要重新設定回來,要將logger.add()sink變數設定為sys.stderr就能在Terminal上顯示,回傳的stderrLogger類型為int,在刪除的時可以將handler_id指定為stderrLogger來刪除,將stderrLogger物件移除之後,在這之後的內容不會再輸出到日誌檔案:

impory sys
stderrLogger = logger.add(sink=sys.stderr, level="TRACE")
logger.remove(handler_id=stderrLogger)

可以調整日誌的等級來控制輸出:

from loguru import logger

logger.trace('This is debug information')
logger.debug('This is debug information')
logger.info('This is info information')
logger.success('This is info information')
logger.warning('This is warn information')
logger.error('This is error information')
logger.critical('This is error information')

如果是在IDE或Terminal上運行會發現,Loguru在輸出不同等級的資訊時,會帶上不同的顏色:

如果要切換記錄不同level的log時,可以透過logger.add在建立時設定:

stderrLogger = logger.add(sys.stderr, level="TRACE")

以下是不同log level的對應關係:

Level Name Sererity Value Logger Method
TRACE 5 logger.trace()
DEBUG 10 logger.debug()
INFO 20 logger.info()
SUCCESS 25 logger.success()
WARNING 30 logger.warning()
ERROR 40 logger.error()
CRITICAL 50 logger.critical()

自定義日誌格式

在使用Loguru記錄日誌時,自訂日誌的儲存檔案名稱與日誌的格式化是常見需求,Loguru提供了非常靈活且直觀的方式來實現這兩個需求,讓開發者能夠依據應用場景,確保日誌文件的命名清晰、內容豐富且具可讀性。

自定義名稱

將日誌寫入文件時,自訂文件名稱可以幫助你組織和管理日誌文件,Loguru 允許在文件名稱中使用變量,特別是利用時間戳來動態產生文件名,這對於需要根據日期或時間進行日誌輪替的應用非常有用。

使用{time}來命名檔案,會自動替換{time}為日誌記錄的當前時間,最終產生的檔案名稱可能會類似於logs/log_2024-09-27_12-34-56.log:

# 使用日期時間作為檔案名稱的一部分
logger.add("logs/log_{time}.log")

也可以自定義時間格式,透過使用time.strftime的時間格式化規則來完成,每天的日誌都會儲存在不同的檔案中,僅根據日期進行輪換。 :

# 自定義時間格式
logger.add("logs/log_{time:YYYY-MM-DD}.log

除了時間,也可以在檔案名稱中使用更多自訂內容,例如進程ID、日誌等級等來產生有意義的檔案名稱,這對於需要記錄多進程應用程式的日誌時特別有用:

# 其他檔案名稱格式變量
logger.add("logs/log_{time}_{level}_{process}.log")

自定義格式

format參數允許自定義每條日誌的輸出格式,對於提升日誌的可讀性以及讓日誌內容更加有用是非常有用的,Loguru提供了一系列的內置變量,可以在中format使用來控制每條日誌的輸出樣式。

可以透過format來控制日誌輸出的格式,包括時間、日誌等級、訊息內容等:

logger.add("logs/log.log", format="{time} {level} {message}")
logger.info('This is info information')

這個範例的日誌輸出格式會顯示時間、日誌等級和訊息內容,最終的日誌可能看起來像這樣:

2024-09-28T13:44:58.839276+0000 INFO This is info information

Loguru 支援多種格式化變量,可以自由組合建立適合自己的日誌輸出格式:

變量 變量說明
{time} 該Log記錄的時間,默認格式為 YYYY-MM-DD HH:mm:ss
{level} Log等級(如 DEBUG、INFO、WARNING、ERROR)
{message} Log訊息內容
{name} 該Log產生的記錄器名稱
{file} 當前程式碼檔案名稱
{line} 該Log所在的程式碼行號
{function} 記錄該Log的函數名稱
{thread} 記錄該Log的thread ID
{process} 記錄該Log的process ID

如果希望在日誌中顯示檔案名稱行號以及函數名稱,這些資訊可以在大型專案中更快地定位Bug或Debug問題:

logger.add("logs/log.log", format="{time} {level} {file}:{line} - {function} - {message}")
logger.info('This is info information')

這樣的日誌輸出會包含詳細的Debug訊息:

2024-09-28T13:46:25.002661+0000 INFO main.py:4 - <module> - This is info information

如果正在開發多線程或多進程應用,記錄thread和process資訊能夠更好地理解日誌的上下文,可以使用{thread}{process}來顯示thread IDprocess ID :

logger.add("logs/log.log", format="{time} {level} {process} {thread} {message}")
logger.info('This is info information')

這樣的日誌輸出會包含thread和process的資訊;

2024-09-28T13:48:08.821860+0000 INFO 2827 139391729250304 This is info information

自動檔案輪換

在日誌管理中,控制日誌檔案的大小、時間間隔或總檔案數量是非常重要的功能,特別是在長時間運行的應用中,過大的日誌檔案不僅難以管理,還會影響系統效能,Loguru提供了內置的rotation (檔案輪換)retention (保留)功能,支援自動將日誌檔案輪換存檔,並且可以根據檔案大小、時間間隔或檔案數量進行控制。

Rotation

當達到一定的條件(如檔案大小時間間隔時),自動將目前日誌檔案關閉並建立新的檔案:

# 當文件大小超過 10 MB 時會自動創建一個新的日誌文件
logger.add("file_{time}.log", rotation="10 MB")

# 每天半夜自動創建一個新的日誌文件
logger.add("file_{time}.log", rotation="00:00")

# 也可以指定其他時間間隔來進行輪替 例如每小時、每分鐘等
# 每小時自動創建一個新的日誌文件
logger.add("file_{time}.log", rotation="1 hour")

Retention

根據特定條件決定要保留的日誌檔案數量時間範圍,超出範圍的舊日誌檔案會自動刪除,防止檔案數量過多佔用過多儲存空間:

# 保留最近 10 個日誌文件 超過部分自動刪除
logger.add("file_{time}.log", retention=10)

# 保留最近 7 天的日誌文件
logger.add("file_{time}.log", retention="7 days")

Compression

Loguru還提供了壓縮功能,當日誌輪替後,舊日誌將會自動壓縮為.zip格式,以節省空間,使用compression參數來啟用此功能:

# 同時使用rotation和retention 以實現更加靈活的日誌管理
# 每天半夜輪替 保留 7 天的文件 並將舊文件壓縮為 .zip 格式
logger.add("file_{time}.log", rotation="00:00", retention="7 days", compression="zip")

logger.catch

Loguru提供了一個非常方便且強大的功能,就是透過函數裝飾器來自動處理錯誤和記錄日誌,這特別適用於想要自動記錄函數進出、錯誤堆疊追蹤、以及避免未捕捉異常導致程序崩潰的場景,logger.catch()是這項功能的核心,可以簡化錯誤處理,並自動記錄程式中的異常。

自動捕捉異常

直接使用 @logger.catch 作為函數的裝飾器,這樣當程式碼中出現未處理的異常時,Loguru會自動捕捉並記錄異常,而程式不會崩潰:

from loguru import logger

@logger.catch
def divide(a, b):
    return a / b

divide(10, 0)
print("-----Keep Running-----")

在上面例子中,divide()函數引發了ZeroDivisionError,由於使用了logger.catch(),這個異常會被捕捉,並完整記錄在日誌中,輸出的日誌將包含詳細的錯誤和堆疊追蹤的訊息:

2024-09-28 13:50:36.823 | ERROR    | __main__:<module>:7 - An error has been caught in function '<module>', process 'MainProcess' (3438), thread 'MainThread' (140529672212480):
Traceback (most recent call last):

> File "/content/main.py", line 7, in <module>
    divide(10, 0)
    └ <function divide at 0x7fcf9c3e24d0>

  File "/content/main.py", line 5, in divide
    return a / b
           │   └ 0
           └ 10

ZeroDivisionError: division by zero
-----Keep Running-----

這種方式不僅能記錄異常,還能包含詳細的堆疊追蹤信息,能快速定位問題。

保護程式碼區塊

除了用作函數裝飾器,logger.catch()還可以用來保護整個應用程式或一大段程式碼,而不僅僅是函數,這樣可以確保在某段程式碼運行過程中,任何未處理的異常都會被捕捉並記錄下來,從而防止任何異常影響整體程式:

from loguru import logger

logger.add("error.log")  # 將日誌寫入檔案

def main():
    with logger.catch():
        10 / 0  # 故意引發除零錯誤

main()

在上面的範例中,當程式引發除零錯誤時,logger.catch()會自動捕捉並將錯誤記錄到error.log中,而不是讓程式崩潰。這樣的使用方式非常適合那些無法通過函數裝飾器直接捕捉的代碼塊,例如某個函數內的多個邏輯分支或多個異步操作:

2024-09-28 14:00:49.070 | ERROR    | __main__:main:6 - An error has been caught in function 'main', process 'MainProcess' (5877), thread 'MainThread' (137382592274432):
Traceback (most recent call last):

  File "/content/main.py", line 9, in <module>
    main()
    └ <function main at 0x7cf2e063fd90>

> File "/content/main.py", line 7, in main
    10 / 0  # 故意引發除零錯誤

ZeroDivisionError: division by zero

強大的參數控制

logger.catch() 提供了一些進階參數來進一步控制異常處理的行為。

reraise 重新引發異常

如果在記錄完錯誤之後,希望將異常重新引發 (讓上層的代碼也能處理該異常),可以使用 reraise=True 參數:

from loguru import logger

@logger.catch(reraise=True)
def divide(a, b):
    return a / b

try:
    divide(10, 0)
except ZeroDivisionError:
    print("捕捉到除零異常")

reraise=True會在捕捉到異常後,重新引發ZeroDivisionError,使得外部的try-except區塊也可以處理這個異常:

2024-09-28 14:09:04.104 | ERROR    | __main__:<module>:8 - An error has been caught in function '<module>', process 'MainProcess' (7869), thread 'MainThread' (134771338235904):
Traceback (most recent call last):

> File "/content/main.py", line 8, in <module>
    divide(10, 0)
    └ <function divide at 0x7a92e36d5f30>

  File "/content/main.py", line 5, in divide
    return a / b
           │   └ 0
           └ 10

ZeroDivisionError: division by zero
捕捉到除零異常

onerror 自定義錯誤處理

還可以使用 onerror 參數來設置一個回調函數,在捕捉到異常時執行自定義的邏輯,例如發送通知或記錄額外信息:

from loguru import logger

def notify_admin(e):
    print(f"系統出錯:{e}")
    print("啟動緊急應變流程、收集故障排除數據、發送郵件...")

@logger.catch(onerror=notify_admin)
def main():
    10 / 0  # 故意引發錯誤

main()

當這段程式執行時,notify_admin()會被調用,並將捕捉到的異常資訊傳遞給它。

2024-09-28 14:21:33.986 | ERROR    | __main__:<module>:11 - An error has been caught in function '<module>', process 'MainProcess' (10861), thread 'MainThread' (137755256123392):
Traceback (most recent call last):

> File "/content/main.py", line 11, in <module>
    main()
    └ <function main at 0x7d49a2cb5fc0>

  File "/content/main.py", line 9, in main
    10 / 0  # 故意引發錯誤

ZeroDivisionError: division by zero
系統出錯:division by zero
啟動緊急應變流程、收集故障排除數據、發送郵件...

捕捉特定異常

logger.catch() 默認會捕捉所有類型的異常,但也可以根據需求選擇性地捕捉特定類型的異常,例如指定只捕捉Exception類型的異常,這涵蓋了大多數標準的異常類型:

from loguru import logger

@logger.catch(Exception)
def divide(a, b):
    return a / b

divide(10, 0)

也可以捕捉某些特定的異常類型,例如ZeroDivisionError:

@logger.catch(ZeroDivisionError)
def divide(a, b):
    return a / b

結論

Loguru是一個簡潔且功能豐富的日誌工具,能有效提升開發過程中的錯誤排查和性能優化。與標準的Python logging相比,Loguru 更加直觀易用,無需繁瑣的配置,即可輕鬆上手。此外,它提供了靈活的自訂格式、自動檔案輪替、壓縮和日誌保留等功能,這些特性對於需要長期運行且對日誌管理要求高的應用尤其有用。

Loguru的logger.catch()進一步簡化了錯誤處理流程免於手動編寫大量 try-except,允許開發者方便地捕捉異常並記錄詳細的堆疊信息,從而提升日誌的可讀性和應用的可維護性,大幅提升了錯誤排查的效率。。