Python日誌庫logging總結-可能是目前為止將logging庫總結的最好的一篇文章

Wizey發表於2018-10-14

在部署專案時,不可能直接將所有的資訊都輸出到控制檯中,我們可以將這些資訊記錄到日誌檔案中,這樣不僅方便我們檢視程式執行時的情況,也可以在專案出現故障時根據執行時產生的日誌快速定位問題出現的位置。

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 型別,LoggerLogRecordFilterHandlerFormatter

型別說明:

Logger:日誌,暴露函式給應用程式,基於日誌記錄器和過濾器級別決定哪些日誌有效。

LogRecord :日誌記錄器,將日誌傳到相應的處理器處理。

Handler :處理器, 將(日誌記錄器產生的)日誌記錄傳送至合適的目的地。

Filter :過濾器, 提供了更好的粒度控制,它可以決定輸出哪些日誌記錄。

Formatter:格式化器, 指明瞭最終輸出中日誌記錄的佈局。

logging流程圖.png

  1. 判斷 Logger 物件對於設定的級別是否可用,如果可用,則往下執行,否則,流程結束。
  2. 建立 LogRecord 物件,如果註冊到 Logger 物件中的 Filter 物件過濾後返回 False,則不記錄日誌,流程結束,否則,則向下執行。
  3. LogRecord 物件將 Handler 物件傳入當前的 Logger 物件,(圖中的子流程)如果 Handler 物件的日誌級別大於設定的日誌級別,再判斷註冊到 Handler 物件中的 Filter 物件過濾後是否返回 True 而放行輸出日誌資訊,否則不放行,流程結束。
  4. 如果傳入的 Handler 大於 Logger 中設定的級別,也即 Handler 有效,則往下執行,否則,流程結束。
  5. 判斷這個 Logger 物件是否還有父 Logger 物件,如果沒有(代表當前 Logger 物件是最頂層的 Logger 物件 root Logger),流程結束。否則將 Logger 物件設定為它的父 Logger 物件,重複上面的 3、4 兩步,輸出父類 Logger 物件中的日誌輸出,直到是 root Logger 為止。

3、日誌輸出格式

日誌的輸出格式可以認為設定,預設格式為下圖所示。

預設日誌輸出格式.png

4、基本使用

logging 使用非常簡單,使用 basicConfig() 方法就能滿足基本的使用需要,如果方法沒有傳入引數,會根據預設的配置建立Logger 物件,預設的日誌級別被設定為 WARNING,預設的日誌輸出格式如上圖,該函式可選的引數如下表所示。

引數名稱 引數描述
filename 日誌輸出到檔案的檔名
filemode 檔案模式,r[+]、w[+]、a[+]
format 日誌輸出的格式
datefat 日誌附帶日期時間的格式
style 格式佔位符,預設為 "%" 和 “{}”
level 設定日誌輸出級別
stream 定義輸出流,用來初始化 StreamHandler 物件,不能 filename 引數一起使用,否則會ValueError 異常
handles 定義處理器,用來建立 Handler 物件,不能和 filename 、stream 引數一起使用,否則也會丟擲 ValueError 異常

示例程式碼如下:

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)官方文件

變數 格式 變數描述
asctime %(asctime)s 將日誌的時間構造成可讀的形式,預設情況下是精確到毫秒,如 2018-10-13 23:24:57,832,可以額外指定 datefmt 引數來指定該變數的格式
name %(name) 日誌物件的名稱
filename %(filename)s 不包含路徑的檔名
pathname %(pathname)s 包含路徑的檔名
funcName %(funcName)s 日誌記錄所在的函式名
levelname %(levelname)s 日誌的級別名稱
message %(message)s 具體的日誌資訊
lineno %(lineno)d 日誌記錄所在的行號
pathname %(pathname)s 完整路徑
process %(process)d 當前程式ID
processName %(processName)s 當前程式名稱
thread %(thread)d 當前執行緒ID
threadName %threadName)s 當前執行緒名稱

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 檔案中提供了 TimedRotatingFileHandlerRotatingFileHandler 類分別可以實現按時間和大小劃分。開啟這個 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 庫是執行緒安全的,但在多程式、多執行緒、多程式多執行緒環境中仍然還有值得考慮的問題,比如,如何將日誌按照程式(或執行緒)劃分為不同的日誌檔案,也即一個程式(或執行緒)對應一個檔案。由於本文篇幅有限,故不在這裡做詳細說明,只是起到引發讀者思考的目的,這些問題我會在另一篇文章中討論。

總結:Python logging 庫設計的真的非常靈活,如果有特殊的需要還可以在這個基礎的 logging 庫上進行改進,建立新的 Handler 類解決實際開發中的問題。

覺得文章還不錯,歡迎關注我的微信公眾號哦,裡面有非常多福利等著你哦。

程式設計心路

相關文章