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

在部署項目時,不可能直接將全部的信息都輸出到控制檯中,咱們能夠將這些信息記錄到日誌文件中,這樣不只方便咱們查看程序運行時的狀況,也能夠在項目出現故障時根據運行時產生的日誌快速定位問題出現的位置。html

一、日誌級別

Python 標準庫 logging 用做記錄日誌,默認分爲六種日誌級別(括號爲級別對應的數值),NOTSET(0)、DEBUG(10)、INFO(20)、WARNING(30)、ERROR(40)、CRITICAL(50)。咱們自定義日誌級別時注意不要和默認的日誌級別數值相同,logging 執行時輸出大於等於設置的日誌級別的日誌信息,如設置日誌級別是 INFO,則 INFO、WARNING、ERROR、CRITICAL 級別的日誌都會輸出。python

二、logging 流程

官方的 logging 模塊工做流程圖以下:shell

從下圖中咱們能夠看出看到這幾種 Python 類型,LoggerLogRecordFilterHandlerFormatter編程

類型說明:安全

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

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 對象,重複上面的 三、4 兩步,輸出父類 Logger 對象中的日誌輸出,直到是 root Logger 爲止。

三、日誌輸出格式

日誌的輸出格式能夠認爲設置,默認格式爲下圖所示。

默認日誌輸出格式.png

四、基本使用

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)
複製代碼

五、自定義 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)

# 分別爲 十、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,在知足大於當前設置的日誌級別後就會輸出導入文件中的日誌。

六、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")
# 省略日誌輸出
複製代碼

七、實戰中的問題

一、中文亂碼

上面的例子中日誌輸出都是英文內容,發現不了將日誌輸出到文件中會有中文亂碼的問題,如何解決到這個問題呢?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)
複製代碼

二、臨時禁用日誌輸出

有時候咱們又不想讓日誌輸出,但在這後又想輸出日誌。若是咱們打印信息用的是 print() 方法,那麼就須要把全部的 print() 方法都註釋掉,而使用了 logging 後,咱們就有了一鍵開關閉日誌的 "魔法"。一種方法是在使用默認配置時,給 logging.disabled() 方法傳入禁用的日誌級別,就能夠禁止設置級別如下的日誌輸出了,另外一種方法時在自定義 Logger 時,Logger 對象的 disable 屬性設爲 True,默認值是 False,也即不由用。

logging.disable(logging.INFO)
複製代碼
logger.disabled = True
複製代碼

三、日誌文件按照時間劃分或者按照大小劃分

若是將日誌保存在一個文件中,那麼時間一長,或者日誌一多,單個日誌文件就會很大,既不利於備份,也不利於查看。咱們會想到能不能按照時間或者大小對日誌文件進行劃分呢?答案確定是能夠的,而且還很簡單,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 類解決實際開發中的問題。

以爲文章還不錯,歡迎關注個人微信公衆號哦,裏面有很是多福利等着你哦。

編程心路
相關文章
相關標籤/搜索