在部署項(xiàng)目時(shí)翼雀,不可能直接將所有的信息都輸出到控制臺(tái)中廷支,我們可以將這些信息記錄到日志文件中嵌灰,這樣不僅方便我們查看程序運(yùn)行時(shí)的情況问词,也可以在項(xiàng)目出現(xiàn)故障時(shí)根據(jù)運(yùn)行時(shí)產(chǎn)生的日志快速定位問題出現(xiàn)的位置森爽。
1恨豁、日志級(jí)別
Python 標(biāo)準(zhǔn)庫 logging 用作記錄日志,默認(rèn)分為六種日志級(jí)別(括號(hào)為級(jí)別對(duì)應(yīng)的數(shù)值)拗秘,NOTSET(0)圣絮、DEBUG(10)、INFO(20)雕旨、WARNING(30)扮匠、ERROR(40)捧请、CRITICAL(50)。我們自定義日志級(jí)別時(shí)注意不要和默認(rèn)的日志級(jí)別數(shù)值相同棒搜,logging 執(zhí)行時(shí)輸出大于等于設(shè)置的日志級(jí)別的日志信息疹蛉,如設(shè)置日志級(jí)別是 INFO,則 INFO力麸、WARNING可款、ERROR、CRITICAL 級(jí)別的日志都會(huì)輸出克蚂。
2闺鲸、logging 流程
官方的 logging 模塊工作流程圖如下:
從下圖中我們可以看出看到這幾種 Python 類型,Logger埃叭、LogRecord摸恍、Filter、Handler赤屋、Formatter立镶。
類型說明:
Logger:日志,暴露函數(shù)給應(yīng)用程序类早,基于日志記錄器和過濾器級(jí)別決定哪些日志有效媚媒。
LogRecord :日志記錄器,將日志傳到相應(yīng)的處理器處理涩僻。
Handler :處理器, 將(日志記錄器產(chǎn)生的)日志記錄發(fā)送至合適的目的地缭召。
Filter :過濾器, 提供了更好的粒度控制,它可以決定輸出哪些日志記錄。
Formatter:格式化器, 指明了最終輸出中日志記錄的布局令哟。
- 判斷 Logger 對(duì)象對(duì)于設(shè)置的級(jí)別是否可用恼琼,如果可用,則往下執(zhí)行屏富,否則晴竞,流程結(jié)束。
- 創(chuàng)建 LogRecord 對(duì)象狠半,如果注冊(cè)到 Logger 對(duì)象中的 Filter 對(duì)象過濾后返回 False噩死,則不記錄日志,流程結(jié)束神年,否則已维,則向下執(zhí)行。
- LogRecord 對(duì)象將 Handler 對(duì)象傳入當(dāng)前的 Logger 對(duì)象已日,(圖中的子流程)如果 Handler 對(duì)象的日志級(jí)別大于設(shè)置的日志級(jí)別垛耳,再判斷注冊(cè)到 Handler 對(duì)象中的 Filter 對(duì)象過濾后是否返回 True 而放行輸出日志信息,否則不放行,流程結(jié)束堂鲜。
- 如果傳入的 Handler 大于 Logger 中設(shè)置的級(jí)別栈雳,也即 Handler 有效,則往下執(zhí)行缔莲,否則哥纫,流程結(jié)束。
- 判斷這個(gè) Logger 對(duì)象是否還有父 Logger 對(duì)象痴奏,如果沒有(代表當(dāng)前 Logger 對(duì)象是最頂層的 Logger 對(duì)象 root Logger)蛀骇,流程結(jié)束。否則將 Logger 對(duì)象設(shè)置為它的父 Logger 對(duì)象读拆,重復(fù)上面的 3擅憔、4 兩步,輸出父類 Logger 對(duì)象中的日志輸出檐晕,直到是 root Logger 為止雕欺。
3、日志輸出格式
日志的輸出格式可以認(rèn)為設(shè)置棉姐,默認(rèn)格式為下圖所示。
4啦逆、基本使用
logging 使用非常簡單伞矩,使用 basicConfig() 方法就能滿足基本的使用需要,如果方法沒有傳入?yún)?shù)夏志,會(huì)根據(jù)默認(rèn)的配置創(chuàng)建Logger 對(duì)象乃坤,默認(rèn)的日志級(jí)別被設(shè)置為 WARNING,默認(rèn)的日志輸出格式如上圖沟蔑,該函數(shù)可選的參數(shù)如下表所示湿诊。
參數(shù)名稱 | 參數(shù)描述 |
---|---|
filename | 日志輸出到文件的文件名 |
filemode | 文件模式,r[+]瘦材、w[+]厅须、a[+] |
format | 日志輸出的格式 |
datefat | 日志附帶日期時(shí)間的格式 |
style | 格式占位符,默認(rèn)為 "%" 和 “{}” |
level | 設(shè)置日志輸出級(jí)別 |
stream | 定義輸出流食棕,用來初始化 StreamHandler 對(duì)象朗和,不能 filename 參數(shù)一起使用,否則會(huì)ValueError 異常 |
handles | 定義處理器簿晓,用來創(chuàng)建 Handler 對(duì)象眶拉,不能和 filename 、stream 參數(shù)一起使用憔儿,否則也會(huì)拋出 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')
輸出結(jié)果如下:
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
傳入常用的參數(shù)忆植,示例代碼如下(這里日志格式占位符中的變量放到后面介紹):
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 ,內(nèi)容如下:
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
但是當(dāng)發(fā)生異常時(shí),直接使用無參數(shù)的 debug()朝刊、info()耀里、warning()、error()坞古、critical() 方法并不能記錄異常信息备韧,需要設(shè)置 exc_info 參數(shù)為 True 才可以,或者使用 exception() 方法痪枫,還可以使用 log() 方法织堂,但還要設(shè)置日志級(jí)別和 exc_info 參數(shù)。
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 模塊,但一般并不能滿足實(shí)際使用吃粒,我們還需要自定義 Logger潦俺。
一個(gè)系統(tǒng)只有一個(gè) Logger 對(duì)象,并且該對(duì)象不能被直接實(shí)例化徐勃,沒錯(cuò)事示,這里用到了單例模式,獲取 Logger 對(duì)象的方法為 getLogger僻肖。
注意:這里的單例模式并不是說只有一個(gè) Logger 對(duì)象肖爵,而是指整個(gè)系統(tǒng)只有一個(gè)根 Logger 對(duì)象,Logger 對(duì)象在執(zhí)行 info()臀脏、error() 等方法時(shí)實(shí)際上調(diào)用都是根 Logger 對(duì)象對(duì)應(yīng)的 info()劝堪、error() 等方法。
我們可以創(chuàng)造多個(gè) Logger 對(duì)象揉稚,但是真正輸出日志的是根 Logger 對(duì)象秒啦。每個(gè) Logger 對(duì)象都可以設(shè)置一個(gè)名字,如果設(shè)置logger = logging.getLogger(__name__)
搀玖,__name__ 是 Python 中的一個(gè)特殊內(nèi)置變量余境,他代表當(dāng)前模塊的名稱(默認(rèn)為 __main__)。則 Logger 對(duì)象的 name 為建議使用使用以點(diǎn)號(hào)作為分隔符的命名空間等級(jí)制度巷怜。
Logger 對(duì)象可以設(shè)置多個(gè) Handler 對(duì)象和 Filter 對(duì)象葛超,Handler 對(duì)象又可以設(shè)置 Formatter 對(duì)象。Formatter 對(duì)象用來設(shè)置具體的輸出格式延塑,常用變量格式如下表所示绣张,所有參數(shù)見 Python(3.7)官方文檔:
變量 | 格式 | 變量描述 |
---|---|---|
asctime | %(asctime)s | 將日志的時(shí)間構(gòu)造成可讀的形式,默認(rèn)情況下是精確到毫秒关带,如 2018-10-13 23:24:57,832侥涵,可以額外指定 datefmt 參數(shù)來指定該變量的格式 |
name | %(name) | 日志對(duì)象的名稱 |
filename | %(filename)s | 不包含路徑的文件名 |
pathname | %(pathname)s | 包含路徑的文件名 |
funcName | %(funcName)s | 日志記錄所在的函數(shù)名 |
levelname | %(levelname)s | 日志的級(jí)別名稱 |
message | %(message)s | 具體的日志信息 |
lineno | %(lineno)d | 日志記錄所在的行號(hào) |
pathname | %(pathname)s | 完整路徑 |
process | %(process)d | 當(dāng)前進(jìn)程ID |
processName | %(processName)s | 當(dāng)前進(jìn)程名稱 |
thread | %(thread)d | 當(dāng)前線程ID |
threadName | %threadName)s | 當(dāng)前線程名稱 |
Logger 對(duì)象和 Handler 對(duì)象都可以設(shè)置級(jí)別沼撕,而默認(rèn) Logger 對(duì)象級(jí)別為 30 ,也即 WARNING芜飘,默認(rèn) Handler 對(duì)象級(jí)別為 0务豺,也即 NOTSET。logging 模塊這樣設(shè)計(jì)是為了更好的靈活性嗦明,比如有時(shí)候我們既想在控制臺(tái)中輸出DEBUG 級(jí)別的日志笼沥,又想在文件中輸出WARNING級(jí)別的日志∪⑴疲可以只設(shè)置一個(gè)最低級(jí)別的 Logger 對(duì)象奔浅,兩個(gè)不同級(jí)別的 Handler 對(duì)象,示例代碼如下:
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')
控制臺(tái)輸出結(jié)果為:
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
文件中輸出內(nèi)容為:
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
創(chuàng)建了自定義的 Logger 對(duì)象,就不要在用 logging 中的日志輸出方法了鉴裹,這些方法使用的是默認(rèn)配置的 Logger 對(duì)象舞骆,否則會(huì)輸出的日志信息會(huì)重復(fù)。
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')
輸出結(jié)果如下(可以看到日志信息被輸出了兩遍):
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 文件時(shí)径荔,如 import test.py
督禽,在滿足大于當(dāng)前設(shè)置的日志級(jí)別后就會(huì)輸出導(dǎo)入文件中的日志。
6总处、Logger 配置
通過上面的例子赂蠢,我們知道創(chuàng)建一個(gè) Logger 對(duì)象所需的配置了,上面直接硬編碼在程序中配置對(duì)象辨泳,配置還可以從字典類型的對(duì)象和配置文件獲取。打開 logging.config Python 文件玖院,可以看到其中的配置解析轉(zhuǎn)換函數(shù)菠红。
從字典中獲取配置信息:
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 格式,或者從網(wǎng)絡(luò)中獲取都是可以的郊酒,只要有相應(yīng)的文件解析器解析配置即可遇绞,下面只展示了 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燎窘、實(shí)戰(zhàn)中的問題
1摹闽、中文亂碼
上面的例子中日志輸出都是英文內(nèi)容,發(fā)現(xiàn)不了將日志輸出到文件中會(huì)有中文亂碼的問題褐健,如何解決到這個(gè)問題呢付鹿?FileHandler 創(chuàng)建對(duì)象時(shí)可以設(shè)置文件編碼,如果將文件編碼設(shè)置為 “utf-8”(utf-8 和 utf8 等價(jià)),就可以解決中文亂碼問題啦舵匾。一種方法是自定義 Logger 對(duì)象俊抵,需要寫很多配置,另一種方法是使用默認(rèn)配置方法 basicConfig()坐梯,傳入 handlers 處理器列表對(duì)象徽诲,在其中的 handler 設(shè)置文件的編碼。網(wǎng)上很多都是無效的方法吵血,關(guān)鍵參考代碼如下:
# 自定義 Logger 配置
handler = logging.FileHandler(filename="test.log", encoding="utf-8")
# 使用默認(rèn)的 Logger 配置
logging.basicConfig(handlers=[logging.FileHandler("test.log", encoding="utf-8")], level=logging.DEBUG)
2谎替、臨時(shí)禁用日志輸出
有時(shí)候我們又不想讓日志輸出,但在這后又想輸出日志践瓷。如果我們打印信息用的是 print() 方法院喜,那么就需要把所有的 print() 方法都注釋掉,而使用了 logging 后晕翠,我們就有了一鍵開關(guān)閉日志的 "魔法"喷舀。一種方法是在使用默認(rèn)配置時(shí),給 logging.disabled() 方法傳入禁用的日志級(jí)別淋肾,就可以禁止設(shè)置級(jí)別以下的日志輸出了硫麻,另一種方法時(shí)在自定義 Logger 時(shí),Logger 對(duì)象的 disable 屬性設(shè)為 True樊卓,默認(rèn)值是 False拿愧,也即不禁用。
logging.disable(logging.INFO)
logger.disabled = True
3碌尔、日志文件按照時(shí)間劃分或者按照大小劃分
如果將日志保存在一個(gè)文件中浇辜,那么時(shí)間一長,或者日志一多唾戚,單個(gè)日志文件就會(huì)很大柳洋,既不利于備份,也不利于查看叹坦。我們會(huì)想到能不能按照時(shí)間或者大小對(duì)日志文件進(jìn)行劃分呢熊镣?答案肯定是可以的,并且還很簡單募书,logging 考慮到了我們這個(gè)需求绪囱。logging.handlers 文件中提供了 TimedRotatingFileHandler 和 RotatingFileHandler 類分別可以實(shí)現(xiàn)按時(shí)間和大小劃分。打開這個(gè) handles 文件莹捡,可以看到還有其他功能的 Handler 類鬼吵,它們都繼承自基類 BaseRotatingHandler。
# TimedRotatingFileHandler 類構(gòu)造函數(shù)
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None):
# RotatingFileHandler 類的構(gòu)造函數(shù)
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)
示例代碼如下:
# 每隔 1000 Byte 劃分一個(gè)日志文件篮赢,備份文件為 3 個(gè)
file_handler = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8")
# 每隔 1小時(shí) 劃分一個(gè)日志文件而柑,interval 是時(shí)間間隔文捶,備份文件為 10 個(gè)
handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10)
Python 官網(wǎng)雖然說 logging 庫是線程安全的,但在多進(jìn)程媒咳、多線程粹排、多進(jìn)程多線程環(huán)境中仍然還有值得考慮的問題,比如涩澡,如何將日志按照進(jìn)程(或線程)劃分為不同的日志文件顽耳,也即一個(gè)進(jìn)程(或線程)對(duì)應(yīng)一個(gè)文件。由于本文篇幅有限妙同,故不在這里做詳細(xì)說明射富,只是起到引發(fā)讀者思考的目的,這些問題我會(huì)在另一篇文章中討論粥帚。
總結(jié):Python logging 庫設(shè)計(jì)的真的非常靈活胰耗,如果有特殊的需要還可以在這個(gè)基礎(chǔ)的 logging 庫上進(jìn)行改進(jìn),創(chuàng)建新的 Handler 類解決實(shí)際開發(fā)中的問題芒涡。
覺得文章還不錯(cuò)柴灯,歡迎關(guān)注我的微信公眾號(hào)哦,里面有非常多福利等著你哦费尽。