Python - 日誌系統詳解

我的Python是從使用開始的,因此很多Python基礎理論並不是很足,之前在使用Celery時,因爲日誌無法正常打印而排查了一天。所以,爲了節省時間,同時知其所以然,是時候系統梳理一下Python中日誌的使用方法了。

基礎

Python日誌位於logging包,其中僅包含兩個module。在不進行任何額外配置的情況下,可以按照如下方式使用

import logging

logging.warn("Hello World!!!")

輸出

WARNING:root:hello

接着來詳細瞭解其使用方式。

日誌級別

在很多Java日誌框架中,將日誌由嚴重要普通,依次爲 Fatal -> Error -> Warning -> Info -> Debug。在Python中,也一行,只不過Fatal不叫Fatal,叫Critical,所以是 Fatal -> Error -> Warning -> Info -> Debug。使用場景也是一樣,如下

DEBUG 細節信息,僅當診斷問題時適用。
INFO 確認程序按預期運行
WARNING 表明有已經或即將發生的意外(例如:磁盤空間不足)。程序仍按預期進行
ERROR 由於嚴重的問題,程序的某些功能已經不能正常執行
CRITICAL 嚴重的錯誤,表明程序已不能繼續執行

Python默認的日誌級別是Warning,默認輸出到控制檯。這意味着,默認情況下,Info和Debug日誌是不會輸出的

小貼士:日誌的輸出級別,確切地說應該是日誌的輸出閾值,即指定級別及以下級別的日誌會輸出,其上的不會輸出。比如設置日誌級別爲Warning,則除Info、Debug外其它日誌都會輸出。

簡單配置

命令行

可以在使用命令啓動python時指定logging的執行級別

# 執行腳本hello.py,日誌級別設置爲INFO
python hello.py --log=INFO

basicConfig方法

一般來說,logging的配置都可以在logging.basicConfig()方法完成,如下展示了常用的配置

# 設置日誌輸出到文件,日誌級別設爲debug,默認爲追加模式
logging.basicConfig(filename='celery.log', level=logging.DEBUG)
# 輸出到文件,覆蓋模式
logging.basicConfig(filename='celery.log', filemode='w', level=logging.DEBUG)

小貼士:logging.basicConfig方法被設置爲只能調用一次,因此針對他的第一次調用是有效的,但以後的調用不會生效。這也是很多問題的發源地:你可能不自覺地多次調用了basicConfig()

日誌格式

還是在basicConfig()中,我們可以配置日誌輸出格式,通過format參數進行設置

# 輸出格式爲asc時間+空格+消息內容
logging.basicConfig(format='%(asctime)s %(message)s')

日誌格式中可以詳細設置的內容可以在這裏找到,我們列出幾個看似很有用的

屬性名 使用格式 作用
asctime %(asctime)s 日誌輸出時間,對閱讀友好的
filename %(filename)s 輸出日誌的文件
funcName %(funcName)s 輸出日誌的方法
levelname %(levelname)s 級別名稱,如DEBUG、LEVEL
lineno %(lineno)d 輸出日誌的源碼行號
message %(message)s 輸出的消息
process %(process)d 進程ID
thread %(thread)d 線程ID

原理

上面所介紹的內容僅解決了記錄的問題,並不能滿足許多實際項目的需求。比如希望針對不同的日誌級別將日誌輸出到不同的位置、當出現ERROR日誌時通過郵件報警、自動記錄ERROR log的個數等。要做到這些需求,我們需要更加深入Python日誌系統,瞭解其基本工作原理。

這裏有一點忌諱的是,在不瞭解Python日誌基本構成的情況下,直接使用網上搜索的方式進行復雜的日誌配置。這樣成功的概率不高,且出問題時往往面臨束手無策的尷尬境地。

Python日誌庫採用模塊化方法,通過幾類組件協同工作,完成日誌從發起到輸出終點的全流程。如下

  • Logger - 記錄器:用戶暴露接口給用戶直接調用。它產生LogRecord對象傳遞給下一個組件。
  • Handler - 處理器:處理Logger產生的LogRecord
  • Filter - 過濾器:更加精確地控制日誌的輸出
  • Formatter - 格式化器:用戶將日誌處理成最終輸出的樣子

其中,Logger是層次結構的,有父子關係。幾個組件的協同處理流程如下圖所示

在這裏插入圖片描述

Logger

Logger類,即記錄器,用於暴露接口給用戶調用,其簡單創建和使用方法如下

import logging

logger = logging.getLogger(__name__)
logger.warning('Hello, World!!!')

創建Logger使用logging.getLogger('name')方法,當Logger實例已存在,則直接返回,不存在則創建。

Logger具有層次結構,主要通過名稱體現,以.分隔。如名爲root的Logger實例是名爲root.hello、root.world的Logger實例的父級。

層級結構的好處在於減少配置的冗餘,以及消息的冒泡傳遞:在子Logger沒有聲明級別的情況下,使用父Logger的級別,以此類推,如果都沒有設置級別,則會使用root的Warning級別;日誌輸出時,除了執行當前Logger的所有邏輯外,還可以傳遞給父Logger,如上圖所示。

接口方面,Logger主要提供配置和消息創建兩類接口

  • 配置

    • Logger.setLevel() 設置當前Logger的日誌級別
    • Logger.addHandler()/removeHandler() 添加/刪除處理器
    • Logger.addFilter()/removeFilter 添加/刪除過濾器
  • 創建消息

    • Logger.debug()/info()/warning()/error()/critical() 創建不同級別的日誌
    • Logger.exception() 輸出ERROR級別的日誌,不同的是還附帶堆棧信息
    • Logger.log() 顯式自定義日誌輸出級別並創建日誌信息

Handler

Handler類,即處理器。決定如何處理Logger發送的消息對象,即將消息對象發送到哪個目標

Handler暴露給用戶的接口如下

  • Handler.setLevel() 設置當前Handler的日誌級別。這意味着,Logger能夠處理的日誌級別和Handler的日誌級別可以不同,即Handler可以選擇性處理日誌消息
  • Handler.setFormatter() 指定格式化器
  • Handler.addFilter()/revmoFilter() 添加/刪除過濾器

Python內建了很多處理器,如下。通過這些Handler,可將日誌輸出到文件、內存、郵件、網絡

在這裏插入圖片描述

Filter

Filter類,即過濾器。Python只定義了一個Filter基類,該類構造器接收一個name參數,其過濾邏輯爲:僅允許與Logger同級或子級的LogRecord通過,其餘被濾出。如Filter名爲A.B,則名稱爲A.B、A.B.C、A.B.D的Logger創建的LogRecord均可通過,A.F的Logger創建的LogRecord將被濾出。

filter = logging.Filter('hello')

要實現自定義的Filter,繼承logging.Filter類,並重寫其filter()方法即可

Formatter

Formatter類,即格式化器。決定了日誌輸出格式,用戶只需要創建它並扔給Handler就好。

formatter = logging.Formatter(fmt='%(filename)s %(funcName)s %(message)s', datefmt=None, style='%')

參數解釋如下

  • fmt - 格式設置,和基礎部分一樣。能夠輸出LogRecord對象的所有屬性。如果臨時忘了,在Formatter類定義註釋中,有詳細的說明。

  • datefmt - 指定日期的格式化,舉例

    '%m/%d/%Y %I:%M:%S %p'
    
  • style - 格式化風格,%和$二選一,兩個風格區別如下。

    # %風格
    "%s %s" % ('hello', 'world')
    # $風格
    "{} {}".format('hello', 'world')
    

LogRecord

LogRecord類,用戶每調用一次Logger的輸出接口,就會創建一個LogRecord類實例,該實例作爲在日誌系統各組件的最小單位。基於其重要性,這裏單獨提了出來。

配置

Python提供了三種配置日誌的方式

  • 使用各組件提供的配置方法手動配置
  • 按照指定格式創建配置文件,然後使用logging.config.fileConfig()讀取
  • 按照指定格式創建配置字典,然後使用logging.config.dictConfig()讀取

下面直接搬運官方文檔給出的三種方式

代碼配置

import logging

# create logger
logger = logging.getLogger('simpleExample')
logger.setLevel(logging.DEBUG)

# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# add formatter to ch
ch.setFormatter(formatter)

# add ch to logger
logger.addHandler(ch)

文件配置

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

文件配置內容格式具有自解釋作用,這裏不做講解。

唯一需要注意的是,在指定處理器的class時,類名稱是相對於Python的日誌記錄模塊(注意是Python的日誌模塊,即logging的位置),或按照導入機制能夠正常導入的絕對import路徑。

如StreamHandler,因它在logging模塊中定義,因此可以直接寫StreamHandler即可。

再如com.github.zou.HelloHandler,屬於自定義,因此需要全路徑。

import logging
import logging.config

# 上面的文件名爲logging.conf
logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

dict配置

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]
import logging
import logging.config
import yaml

# 將yaml文件讀取爲dict,再進行配置
config = yaml.load(open('logging.yml'), Loader=yaml.FullLoader)
logging.config.dictConfig('logging.yml')

# create logger
logger = logging.getLogger('simpleExample')

有坑

配置覆蓋問題

使用fileConfig()時,帶有默認參數disable_existing_logger=True,因此之前的除root之外的所有Logger全都會被禁用,這一點要非常注意。如果不希望如此,可手動將其設置爲False


def fileConfig(fname, defaults=None, disable_existing_loggers=True):
    """
    Read the logging configuration from a ConfigParser-format file.

    This can be called several times from an application, allowing an end user
    the ability to select from various pre-canned configurations (if the
    developer provides a mechanism to present the choices and load the chosen
    configuration).
    """
. . . . . .

無配置注意事項

如果對應的Logger沒有進行配置,會出現找不到對應處理器來處理消息的情況。根據版本不同會有如下表現

Python版本<3.2時

  • 若logging.raiseException爲False,則靜默丟棄消息
  • 若logging.raiseException爲True,則打印“無法找到記錄器的處理程序”

Python版本>=3.2時

  • 使用logging.lastResort中存儲的處理器進行輸出。該處理器與任何Logger都沒有關聯,直接將描述信息寫入sys.sterr,並且信息不會被格式化,輸出級別爲Warning
  • 如果我們將logging.lastResort手動設置爲None,其表現將和3.2之前的版本一致。

參考文檔

  1. Python日誌教程
  2. 日誌操作手冊
  3. 日誌格式化內容
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章