1.介紹
在 Python 中有一個標準的 logging 模塊,我們可以使用它來進行標註的日誌記錄,利用它我們可以更方便地進行日誌記錄,同時還可以做更方便的級別區分以及一些額外日誌信息的記錄,如時間、運行模塊信息等。
1.1 架構
整個日誌記錄的框架可以分爲這麼幾個部分:
- Logger 記錄器,暴露了應用程序代碼能直接使用的接口。
- Handler 處理器,將(記錄器產生的)日誌記錄發送至合適的目的地。
- Filter 過濾器,提供了更好的粒度控制,它可以決定輸出哪些日誌記錄。
- Formatter 格式化器,指明瞭最終輸出中日誌記錄的佈局。
1.1.1 Logger 記錄器
Logger是一個樹形層級結構,在使用接口debug,info,warn,error,critical
之前必須創建Logger實例,即創建一個記錄器,如果沒有顯式的進行創建,則默認創建一個root logger,並應用默認的日誌級別(WARN),處理器Handler(StreamHandler,即將日誌信息打印輸出在標準輸出上),和格式化器Formatter(默認的格式即爲第一個簡單使用程序中輸出的格式)。
創建方法:
logger = logging.getLogger(logger_name)
創建Logger實例後,可以使用以下方法進行日誌級別設置,增加處理器Handler。
logger.setLevel(logging.ERROR) #設置日誌級別爲ERROR,即只有日誌級別大於等於ERROR的日誌纔會輸出
logger.addHandler(handler_name) # 爲Logger實例增加一個處理器
logger.removeHandler(handler_name) # 爲Logger實例刪除一個處理器
1.1.2 Handler 處理器
Handler處理器類型有很多種,比較常用的有三個,StreamHandler
,FileHandler
,NullHandler
。
創建StreamHandler之後,可以通過使用以下方法設置日誌級別,設置格式化器Formatter,增加或刪除過濾器Filter。
ch.setLevel(logging.WARN) #指定日誌級別,低於WARN級別的日誌將被忽略
ch.setFormatter(formatter_name) #設置一個格式化器formatter
ch.addFilter(filter_name) #增加一個過濾器,可以增加多個
ch.removeFilter(filter_name) #刪除一個過濾器
StreamHandler
創建方法:
sh = logging.StreamHandler(stream=None)
FileHandler
創建方法:
fh = logging.FileHandler(filename, mode='a', encoding=None, delay=False)
NullHandler
NullHandler類位於核心logging包,不做任何的格式化或者輸出。
本質上它是個“什麼都不做”的handler,由庫開發者使用。
logging 模塊提供的 全部Handler 有:
StreamHandler:logging.StreamHandler; 日誌輸出到流,可以是 sys.stderr,sys.stdout 或者文件。
FileHandler:logging.FileHandler; 日誌輸出到文件。
BaseRotatingHandler:logging.handlers.BaseRotatingHandler; 基本的日誌回滾方式。
RotatingHandler:logging.handlers.RotatingHandler;日誌回滾方式,支持日誌文件最大數量和日誌文件回滾。
TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日誌回滾方式,在一定時間區域內回滾日誌文件。
SocketHandler:logging.handlers.SocketHandler;遠程輸出日誌到TCP/IP sockets。
DatagramHandler:logging.handlers.DatagramHandler;遠程輸出日誌到UDP sockets。
SMTPHandler:logging.handlers.SMTPHandler;遠程輸出日誌到郵件地址。
SysLogHandler:logging.handlers.SysLogHandler;日誌輸出到syslog。
NTEventLogHandler:logging.handlers.NTEventLogHandler;遠程輸出日誌到Windows NT/2000/XP的事件日誌。
MemoryHandler:logging.handlers.MemoryHandler;日誌輸出到內存中的指定buffer。
HTTPHandler:logging.handlers.HTTPHandler;通過”GET”或者”POST”遠程輸出到HTTP服務器。
1.1.3 Formatter 格式化器
使用Formatter對象設置日誌信息最後的規則、結構和內容,默認的時間格式爲%Y-%m-%d %H:%M:%S
。
創建方法:
formatter = logging.Formatter(fmt=None, datefmt=None)
其中,fmt是消息的格式化字符串,datefmt是日期字符串。如果不指明fmt,將使用’%(message)s’。如果不指明datefmt,將使用ISO8601日期格式。
1.1.4 Filter 過濾器
Handlers和Loggers可以使用Filters來完成比級別更復雜的過濾。Filter基類只允許特定Logger層次以下的事件。例如用‘A.B’初始化的Filter允許Logger ‘A.B’, ‘A.B.C’, ‘A.B.C.D’, ‘A.B.D’等記錄的事件,logger‘A.BB’, ‘B.A.B’ 等就不行。 如果用空字符串來初始化,所有的事件都接受。
創建方法:
filter = logging.Filter(name='')
熟悉了這些概念之後,有另外一個比較重要的事情必須清楚,即Logger是一個樹形層級結構;
Logger可以包含一個或多個Handler和Filter,即Logger與Handler或Fitler是一對多的關係;
一個Logger實例可以新增多個Handler,一個Handler可以新增多個格式化器或多個過濾器,而且日誌級別將會繼承。
2. 優點
總的來說 logging 模塊相比 print 有這麼幾個優點:
- 可以在 logging 模塊中設置日誌等級,在不同的版本(如開發環境、生產環境)上通過設置不同的輸出等級來記錄對應的日誌,非常靈活。
- print 的輸出信息都會輸出到標準輸出流中,而 logging 模塊就更加靈活,可以設置輸出到任意位置,如寫入文件、寫入遠程服務器等。
- logging 模塊具有靈活的配置和格式化功能,如配置輸出當前模塊信息、運行時間等,相比 print 的字符串格式化更加方便易用。
3. 用法
3.1 默認輸出
高於WARNING的日誌信息纔會輸出
$ cat log1.py
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')
輸出:
$ python log1.py
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
3.2 日誌配置格式輸出
配置方式
- 顯式創建記錄器Logger、處理器Handler和格式化器Formatter,並進行相關設置;
- 通過簡單方式進行配置,使用basicConfig()函數直接進行配置;
- 通過配置文件進行配置,使用fileConfig()函數讀取配置文件;
- 通過配置字典進行配置,使用dictConfig()函數讀取配置信息;
- 通過網絡進行配置,使用listen()函數進行網絡配置。
basicConfig關鍵字參數
關鍵字 描述
filename 創建一個FileHandler,使用指定的文件名,而不是使用StreamHandler。
filemode 如果指明瞭文件名,指明打開文件的模式(如果沒有指明filemode,默認爲'a')。
format handler使用指明的格式化字符串。
datefmt 使用指明的日期/時間格式。
level 指明根logger的級別。
stream 使用指明的流來初始化StreamHandler。該參數與'filename'不兼容,如果兩個都有,'stream'被忽略。
format格式
格式 描述
%(levelno)s 打印日誌級別的數值
%(levelname)s 打印日誌級別名稱
%(pathname)s 打印當前執行程序的路徑
%(filename)s 打印當前執行程序名稱
%(funcName)s 打印日誌的當前函數
%(lineno)d 打印日誌的當前行號
%(asctime)s 打印日誌的時間
%(thread)d 打印線程id
%(threadName)s 打印線程名稱
%(process)d 打印進程ID
%(message)s 打印日誌信息
3.2.1 配置輸出級別並指定到文件
$ cat log2.py
#!/usr/local/bin/python
# -*- coding:utf-8 -*-
import logging
# 通過下面的方式進行簡單配置輸出方式與日誌級別
logging.basicConfig(filename='logger.log', level=logging.INFO)
logging.debug('debug message')
logging.info('info message')
logging.warn('warn message')
logging.error('error message')
logging.critical('critical message')
輸出:
$ python log2.py
$ cat logger.log
INFO:root:info message #info級別也能輸出
WARNING:root:warn message
ERROR:root:error message
CRITICAL:root:critical message
3.2.2 聲明瞭一個 Logger 對象,它就是日誌輸出的主類
$ cat log3.py
#!/usr/local/bin/python
# -*- coding:utf-8 -*-
import logging
# 通過下面的方式進行簡單配置輸出方式與日誌級別
logging.basicConfig(filename='logger.log', level=logging.INFO)
logger = logging.getLogger(__name__)
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
輸出:
$ python log3.py
$ cat logger.log
INFO:__main__:info message
WARNING:__main__:warn message
ERROR:__main__:error message
CRITICAL:__main__:critical message
3.2.3 格式化輸出
$ cat log4.py
import logging
logging.basicConfig(filename="logger.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
輸出:
$ python log4.py
$ cat logger.log
17-49-2020 13:49:16 __main__:DEBUG:debug message
17-49-2020 13:49:16 __main__:INFO:info message
17-49-2020 13:49:16 __main__:WARNING:warn message
17-49-2020 13:49:16 __main__:ERROR:error message
17-49-2020 13:49:16 __main__:CRITICAL:critical message
3.3 Handler配置
3.3.1 handler配置格式輸出
$ cat log5.py
import logging
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
handler = logging.FileHandler('output.log')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')
沒有再使用 basicConfig 全局配置,而是先聲明瞭一個 Logger 對象,然後指定了其對應的 Handler 爲 FileHandler 對象,然後 Handler 對象還單獨指定了 Formatter 對象單獨配置輸出格式,最後給 Logger 對象添加對應的 Handler 即可,最後可以發現日誌就會被輸出到 output.log 中.
$ python log5.py
$ cat output.log
2020-05-17 13:58:41,039 - __main__ - INFO - This is a log info
2020-05-17 13:58:41,039 - __main__ - WARNING - Warning exists
2020-05-17 13:58:41,039 - __main__ - INFO - Finish
2020-05-17 14:01:15,909 - __main__ - INFO - This is a log info
2020-05-17 14:01:15,909 - __main__ - WARNING - Warning exists
2020-05-17 14:01:15,909 - __main__ - INFO - Finish
3.3.2 多種handle輸出
$ cat log6.py
import logging
from logging.handlers import HTTPHandler
import sys
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)
# StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)
# FileHandler
file_handler = logging.FileHandler('output.log')
file_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# HTTPHandler
http_handler = HTTPHandler(host='localhost:80', url='log', method='POST')
logger.addHandler(http_handler)
# Log
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')
輸出:
$ python log6.py
This is a log info
Debugging
Warning exists
Finish
$ cat output.log
2020-05-17 14:05:48,809 - __main__ - INFO - This is a log info
2020-05-17 14:05:48,849 - __main__ - WARNING - Warning exists
2020-05-17 14:05:48,851 - __main__ - INFO - Finish
3.4 捕獲 Traceback
$ cat log7.py
import logging
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)
# Formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# FileHandler
file_handler = logging.FileHandler('result.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# StreamHandler
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
# Log
logger.info('Start')
logger.warning('Something maybe fail.')
try:
result = 10 / 0
except Exception:
logger.error('Faild to get result', exc_info=True)
#logger.error('Faild to get result')
logger.info('Finished')
在 error() 方法中添加了一個參數,將 exc_info 設置爲了 True,這樣我們就可以輸出執行過程中的信息了,即完整的 Traceback 信息。
$ python log7.py
2020-05-17 14:13:02,217 - __main__ - INFO - Start
2020-05-17 14:13:02,217 - __main__ - WARNING - Something maybe fail.
2020-05-17 14:13:02,217 - __main__ - ERROR - Faild to get result
Traceback (most recent call last):
File "log7.py", line 23, in <module>
result = 10 / 0
ZeroDivisionError: integer division or modulo by zero
2020-05-17 14:13:02,217 - __main__ - INFO - Finished
3.5 配置共享
在寫項目的時候,我們肯定會將許多配置放置在許多模塊下面,這時如果我們每個文件都來配置 logging 配置那就太繁瑣了,logging 模塊提供了父子模塊共享配置的機制,會根據 Logger 的名稱來自動加載父模塊的配置。
$ vim main.py
import logging
import core
logger = logging.getLogger('main')
logger.setLevel(level=logging.DEBUG)
# Handler
handler = logging.FileHandler('result.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info('Main Info')
logger.debug('Main Debug')
logger.error('Main Error')
core.run()
這裏我們配置了日誌的輸出格式和文件路徑,同時定義了 Logger 的名稱爲 main,然後引入了另外一個模塊 core,最後調用了 core 的 run() 方法。
$ vim core.py
import logging
logger = logging.getLogger('main.core')
def run():
logger.info('Core Info')
logger.debug('Core Debug')
logger.error('Core Error')
這裏我們定義了 Logger 的名稱爲 main.core,注意這裏開頭是 main,即剛纔我們在 main.py 裏面的 Logger 的名稱,這樣 core.py 裏面的 Logger 就會複用 main.py 裏面的 Logger 配置,而不用再去配置一次了.
$ python main.py
$ cat result.log
2020-05-17 14:30:48,559 - main - INFO - Main Info
2020-05-17 14:30:48,559 - main - ERROR - Main Error
2020-05-17 14:30:48,559 - main.core - INFO - Core Info
2020-05-17 14:30:48,559 - main.core - ERROR - Core Error
3.6 文件配置
常見的配置文件有 ini 格式、yaml 格式、JSON 格式,這裏以yaml爲例。將配置寫入到配置文件,然後運行時讀取配置文件裏面的配置,這樣是更方便管理和維護.
3.6.1 yaml配置文件
$ vim config.yaml
version: 1
formatters:
brief:
format: "%(asctime)s - %(message)s"
simple:
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
console:
class : logging.StreamHandler
formatter: brief
level : INFO
stream : ext://sys.stdout
file:
class : logging.FileHandler
formatter: simple
level: DEBUG
filename: debug.log
error:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: simple
filename: error.log
maxBytes: 10485760
backupCount: 20
encoding: utf8
loggers:
main.core:
level: DEBUG
handlers: [console, file, error]
root:
level: DEBUG
handlers: [console]
這裏我們定義了 formatters、handlers、loggers、root 等模塊,實際上對應的就是各個 Formatter、Handler、Logger 的配置,參數和它們的構造方法都是相同的。
定義一個主入口文件,main.py.
$ vim main.py
import logging
import core
import yaml
import logging.config
import os
import io
def setup_logging(default_path='config.yaml', default_level=logging.INFO):
path = default_path
if os.path.exists(path):
with io.open(path, 'r', encoding='utf-8') as f:
config = yaml.load(f)
logging.config.dictConfig(config)
else:
logging.basicConfig(level=default_level)
def log():
logging.debug('Start')
logging.info('Exec')
logging.info('Finished')
if __name__ == '__main__':
yaml_path = 'config.yaml'
setup_logging(yaml_path)
log()
core.run()
這裏我們定義了一個 setup_logging() 方法,裏面讀取了 yaml 文件的配置,然後通過 dictConfig() 方法將配置項傳給了 logging 模塊進行全局初始化。
另外這個模塊還引入了另外一個模塊 core,所以我們定義 core.py 如下:
import logging
logger = logging.getLogger('main.core')
def run():
logger.info('Core Info')
logger.debug('Core Debug')
logger.error('Core Error')
輸出:
$ python main.py
2020-05-17 14:47:09,905 - Exec
2020-05-17 14:47:09,905 - Finished
2020-05-17 14:47:09,905 - Core Info
2020-05-17 14:47:09,905 - Core Info
2020-05-17 14:47:09,905 - Core Error
2020-05-17 14:47:09,905 - Core Error
$ cat error.log
2020-05-17 14:47:09,905 - main.core - ERROR - Core Error
$ cat debug.log
2020-05-17 14:47:09,905 - main.core - INFO - Core Info
2020-05-17 14:47:09,905 - main.core - DEBUG - Core Debug
2020-05-17 14:47:09,905 - main.core - ERROR - Core Error
3.6.2 ini配置文件
下面只展示了 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")
logger.info('Core Info')
logger.debug('Core Debug')
logger.error('Core Error')
輸出:
$ python test.py
2020-05-17 15:07:47,328 - sampleLogger - INFO - Core Info
2020-05-17 15:07:47,328 - sampleLogger - DEBUG - Core Debug
2020-05-17 15:07:47,328 - sampleLogger - ERROR - Core Error
3.7 字符串拼接
format() 構造與佔位符用%
$ log8.py
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# bad
logging.debug('Hello {0}, {1}!'.format('World', 'Congratulations'))
# good
logging.debug('Hello %s, %s!', 'World', 'Congratulations')
運行結果如下:
$ python log8.py
2020-05-17 14:55:29,709 - root - DEBUG - Hello World, Congratulations!
2020-05-17 14:55:29,709 - root - DEBUG - Hello World, Congratulations!
3.8 異常處理
$ cat log9.py
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
try:
result = 5 / 0
except Exception as e:
# bad
logging.error('Error: %s', e)
# good
logging.error('Error', exc_info=True)
# good
logging.exception('Error')
結果輸出:
$ python log9.py
2020-05-17 14:56:51,027 - root - ERROR - Error: integer division or modulo by zero
2020-05-17 14:56:51,027 - root - ERROR - Error
Traceback (most recent call last):
File "log9.py", line 6, in <module>
result = 5 / 0
ZeroDivisionError: integer division or modulo by zero
2020-05-17 14:56:51,028 - root - ERROR - Error
Traceback (most recent call last):
File "log9.py", line 6, in <module>
result = 5 / 0
ZeroDivisionError: integer division or modulo by zero
3.9 日誌文件按照時間劃分或者按照大小劃分
如果將日誌保存在一個文件中,那麼時間一長,或者日誌一多,單個日誌文件就會很大,既不利於備份,也不利於查看。我們會想到能不能按照時間或者大小對日誌文件進行劃分呢?答案肯定是可以的,並且還很簡單,logging 考慮到了我們這個需求。logging.handlers 文件中提供了 TimedRotatingFileHandler 和 RotatingFileHandler 類分別可以實現按時間和大小劃分。打開這個 handles 文件,可以看到還有其他功能的 Handler 類,它們都繼承自基類 BaseRotatingHandler。
3.9.1 TimedRotatingFileHandler構造函數
定義如下:
TimedRotatingFileHandler(filename [,when [,interval [,backupCount]]])
示例:
# 每隔 1小時 劃分一個日誌文件,interval 是時間間隔,備份文件爲 10 個
handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10)
- filename: 是輸出日誌文件名的前綴,比如log/myapp.log
- when :是一個字符串的定義如下:
“S”: Seconds
“M”: Minutes
“H”: Hours
“D”: Days
“W”: Week day (0=Monday)
“midnight”: Roll over at midnight - interval :是指等待多少個單位when的時間後,Logger會自動重建文件
- backupCount:是保留日誌個數。默認的0是不會自動刪除掉日誌。若設3,則在文件的創建過程中庫會判斷是否有超過這個3,若超過,則會從最先創建的開始刪除。
$ cat log10.py
#!/usr/bin/python
#---coding:utf-8
import logging
import logging.handlers
import datetime,time
#logging 初始化工作
logger = logging.getLogger("zjlogger")
logger.setLevel(logging.DEBUG)
# 添加TimedRotatingFileHandler
# 定義一個1秒換一次log文件的handler
# 保留3箇舊log文件
rf_handler = logging.handlers.TimedRotatingFileHandler(filename="all.log",when='S',interval=1,\
backupCount=3)
rf_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s"))
#在控制檯打印日誌
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(rf_handler)
logger.addHandler(handler)
while True:
logger.debug('debug message')
logger.info('info message')
logger.warning('warning message')
logger.error('error message')
logger.critical('critical message')
time.sleep(1)
$ ls
all.log all.log.2020-05-17_15-21-55 all.log.2020-05-17_15-21-56 all.log.2020-05-17_15-21-57 log10.py
3.9.2 RotatingFileHandler 構造函數
示例:
# 每隔 1000 Byte 劃分一個日誌文件,備份文件爲 3 個
file_handler = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8")
複製代碼
$ cat log11.py
#!coding:utf-8
#!/usr/bin/env python
import time
import logging
import logging.handlers
# logging初始化工作
logging.basicConfig()
# myapp的初始化工作
myapp = logging.getLogger('myapp')
myapp.setLevel(logging.INFO)
# 寫入文件,如果文件超過100個Bytes,僅保留5個文件。
handler = logging.handlers.RotatingFileHandler(
'logs/myapp.log', maxBytes=100, backupCount=5)
myapp.addHandler(handler)
while True:
time.sleep(0.01)
myapp.info("file test")
輸出:
$ python log11.py
INFO:myapp:file test
INFO:myapp:file test
INFO:myapp:file test
INFO:myapp:file test
.....
$ ls logs
myapp.log myapp.log.1 myapp.log.2 myapp.log.3 myapp.log.4 myapp.log.5
參考鏈接:
進擊的Coder