python Logging日志记录模块详解
寫在篇前
??logging是Python的一個標準庫,其中定義的函數(shù)和類為應(yīng)用程序和庫的開發(fā)實現(xiàn)了一個靈活的事件日志系統(tǒng)。Python logging 的配置由四個部分組成:Logger、Handlers、Filter、Formatter。本篇博客將依次介紹這四個主要部分以及l(fā)ogging的基本應(yīng)用。
??在開始之前,我們有必要先了解一下,什么時候我們才有必要使用logging模塊,什么時候拋出異常(raise Exception),什么時候使用簡單的print函數(shù)即可,官方文檔給我們總結(jié)了一個表格:
?
| 命令行終端輸出或則程序一般的輸出情景 | print() |
| 報告程序正常運行期間發(fā)生的事件(例如,狀態(tài)監(jiān)視或故障調(diào)查) | logging.info()(或則 logging.debug() 用于診斷目的細節(jié)輸出) |
| 發(fā)出有關(guān)特定運行時事件的警告 | warnings.warn() : 用于如果問題是可以避免的,且應(yīng)修改應(yīng)用程序以消除警告的情景;logging.warning()用于如果應(yīng)用程序無法處理該情況,但仍應(yīng)注意該事件的情景。 |
| 報告有關(guān)特定運行時事件的錯誤 | Raise an exception |
| 報告在不引發(fā)異常的情況下抑制錯誤(例如,長時間運行的服務(wù)器進程中的錯誤處理程序) | logging.error(), logging.exception()或則logging.critical() 適用于特定錯誤和應(yīng)用程序域 |
Loggers
??logger是暴露給代碼進行日志操作的接口。需要注意的是,logger不應(yīng)該直接實例化,而應(yīng)通過模塊級函數(shù)logging.getLogger(name)創(chuàng)建。如果name是具有層級結(jié)構(gòu)的命名方式,則logger之間也會有層級關(guān)系。如name為foo.bar,foo.bar.baz, foo.bam 的logger是foo的子孫,默認子logger日志會向父logger傳播,可以通過logger.propagate=False禁止;對具有相同名稱的getLogger()的多次調(diào)用將始終返回同一Logger對象的引用。logger對象的功能包括以下三項:
??另外,需要理清以下兩個概念:
Log Record
Each message that is written to the Logger is a Log Record. 可以使用makeLogRecord()函數(shù)創(chuàng)建record對象(一般用不上),record對象常用的屬性如下,全部屬性請參考官方文檔:
- record.levelname # record level
- record.levelno # record level number
- record.msg # record承載的日志消息
- record.pathname # emit該日志消息的程序文件
- record.lineno # emit該日志消息的程序行號
- record.getMessage() # 同record.msg
?從python 3.2起,logging模塊提供了工廠函數(shù)getLogRecordFactory() 和setLogRecordFactory()方便、支持用戶自定義record屬性。
old_factory = logging.getLogRecordFactory()def record_factory(*args, **kwargs):record = old_factory(*args, **kwargs)record.custom_attribute = 0xdecafbadreturn recordlogging.setLogRecordFactory(record_factory)Log Level
??每個logger都需要設(shè)置一個log level,該log level描述了logger將處理日志消息的級別; 每個log record也具有一個log Level,指示該特定日志消息的級別。 log record還可以包含被記錄事件的metadata,包括諸如堆棧跟蹤或錯誤代碼之類的詳細信息。如果設(shè)置了logger的log level,系統(tǒng)便只會輸出 level 數(shù)值大于或等于該 level 的的日志結(jié)果,例如我們設(shè)置了輸出日志 level 為 INFO,那么輸出級別小于 INFO 的日志,如DEBUG 和 NOSET 級別的消息不會輸出。logger還有一個effective level的概念,如果logger沒有顯式設(shè)置log level,則使用其parent logger的log level作為其effective level,其中root logger的默認log level是WARNING。
-
NOTEST:lowest level
# Level Num 0logger.setLevel(level=logging.NOTEST) -
DEBUG: Low level system information for debugging purposes
# Level Num 10logger.setLevel(level=logging.DEBUGlogger.debug('Debugging') -
INFO: General system information
# Level Num 20logger.setLevel(level=logging.INFO)logger.info('This is a log info') -
WARNING: Information describing a minor problem that has occurred.
# Level Num 30,默認是該levellogger.setLevel(level=logging.WARNING)logger.warning('Warning exists')logger.warn('Warning exists') # deprecated -
ERROR: Information describing a major problem that has occurred.
# Level Num 40logger.setLevel(level=logging.ERROR)logger.error('some error occur') -
CRITICAL: Information describing a critical problem that has occurred.
# Level Num 50logger.setLevel(level=logging.NOTEST)logger.critical('critical err occur')logger.fatal('fatal error occur')
??上面各個函數(shù)還有一個統(tǒng)一的調(diào)用方式logger.log(level, msg, exc_info=True),其中,level是指上面標注的level num,exc_info指示是否打印執(zhí)行信息;Logger.exception()創(chuàng)建與 Logger.error()相似的日志消息,不同之處是, Logger.exception()同時還記錄當前的堆棧追蹤,請僅從異常處理程序調(diào)用此方法。
??logger對象還有一些其他屬性、方法值得關(guān)注:
>>>logger = logging.getLogger('main') # 如果不指定name將會返回root logger >>>logger.setLevel(level=logging.DEBUG)>>>logger.disabled # False >>>logger.propagate=False >>>logger.getEffectiveLevel() # 10 >>>logger.isEnabledFor(20) # True>>>logging.disable(20) >>>logger.isEnabledFor(20) # False >>>logger.isEnabledFor(30) # True>>>child_logger = logger.getChild('def.ghi') # <Logger main.def.ghi (DEBUG)># Filter addFilter(filter) removeFilter(filter)# handler logger.hasHandlers() # False addHandler(hdlr) removeHandler(hdlr)??關(guān)于上面各種Level的調(diào)用情景,官方文檔也給出了相應(yīng)說明:
| DEBUG | Detailed information, typically of interest only when diagnosing problems. |
| INFO | Confirmation that things are working as expected. |
| WARNING | An indication that something unexpected happened, or indicative of some problem in the near future (e.g. ‘disk space low’). The software is still working as expected. |
| ERROR | Due to a more serious problem, the software has not been able to perform some function. |
| CRITICAL | A serious error, indicating that the program itself may be unable to continue running. |
Handlers
?Once a logger has determined that a message needs to be processed, it is passed to a Handler. The handler is the engine that determines what happens to each message in a logger. Like loggers, handlers also have a log level. If the log level of a log record doesn’t meet or exceed the level of the handler, the handler will ignore the message.
??handler對象負責將適當?shù)娜罩鞠?#xff08;基于日志消息的log level)分發(fā)給handler的指定目標,如文件、標準輸出流等。logging模塊中主要定義了以下Handlers,其中StreamHandler和FileHandler最為常用。
logging.StreamHandler # 日志輸出到流,可以是 sys.stderr,sys.stdout 或者文件。 logging.FileHandler # 日志輸出到文件。 logging.handlers.BaseRotatingHandler # 基本的日志回滾方式。 logging.handlers.RotatingHandler # 日志回滾方式,支持日志文件最大數(shù)量和日志文件回滾。 logging.handlers.TimeRotatingHandler # 日志回滾方式,在一定時間區(qū)域內(nèi)回滾日志文件。 logging.handlers.SocketHandler # 遠程輸出日志到TCP/IP sockets。 logging.handlers.DatagramHandler # 遠程輸出日志到UDP sockets。 logging.handlers.SMTPHandler # 遠程輸出日志到郵件地址。 logging.handlers.SysLogHandler # 日志輸出到syslog。 logging.handlers.NTEventLogHandler # 遠程輸出日志到Windows NT/2000/XP的事件日志。 logging.handlers.MemoryHandler # 日志輸出到內(nèi)存中的指定buffer。 logging.handlers.HTTPHandler # 通過”GET”或者”POST”遠程輸出到HTTP服務(wù)器。 logging.NullHandler??以FileHandler為例,創(chuàng)建Handler對象并設(shè)置log Level
>>> handler = logging.FileHandler('result.log') >>> handler.setLevel(logging.DEBUG) >>> handler.setFormatter(fmt) # fmt是一個Formatter對象,下面再講# Filter addFilter(filter) removeFilter(filter)Filters
?A filter is used to provide additional control over which log records are passed from logger to handler. Filters can be installed on loggers or on handlers; multiple filters can be used in a chain to perform multiple filtering actions.
??logging標準庫只提供了一個base Filter,其構(gòu)造函數(shù)__init__接收name參數(shù),默認的行為是名為name的logger極其子logger的消息能通過過濾,其余皆會被濾掉。當然我們也可以根據(jù)具體的業(yè)務(wù)邏輯自定義Filter,并且也非常簡單,只需要繼承l(wèi)ogging.Filter類重寫filter方法即可,filter方法接收record對象作為參數(shù),返回True代表通過過濾,返回False表示該record被過濾。
import logginglogger = logging.getLogger(__name__) logger.setLevel(level=logging.INFO) handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) handler.set_name('output-log') formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s',datefmt='%Y/%m/%d %H:%M:%S',style='%')class MyFilter(logging.Filter):def filter(self, record):if 'result' in record.msg:return Falsereturn Truehandler.setFormatter(formatter) handler.addFilter(MyFilter('aa')) logger.addHandler(handler)try:result = 10 / 0 except:logger.error('Faild to get result')logger.error('Faild')# 輸出: 2019/07/13 20:28:51 - __main__ - ERROR - Faild??從python3.2起,自定義filter也可以不繼承l(wèi)ogging.Filter,只需要定義一個函數(shù)并同樣綁定即可:
def _filter(record):if 'result' in record.msg:return Falsereturn Truehandler.addFilter(_filter)Formatters
??Formatter用來規(guī)定Log record文本的格式,其使用python formatting string來規(guī)定具體格式。在默認情況下,logging模塊的輸出格式如下:
import logging logging.warning('%s before you %s', 'Look', 'leap!')# WARNING:root:Look before you leap!??但是,默認的輸出格式不一定能滿足我們的需求,我們可以通過Formatter自定義輸出格式,在日志中添加更多豐富的信息,一些常見的項(實際上以下都是Log Record的屬性)如下所示:
%(levelno)s:打印日志級別的數(shù)值。%(levelname)s:打印日志級別的名稱。%(pathname)s:打印當前執(zhí)行程序的路徑,其實就是sys.argv[0]。%(filename)s:打印當前執(zhí)行程序名。%(funcName)s:打印日志的當前函數(shù)。%(lineno)d:打印日志的當前行號。%(asctime)s:打印日志的時間。%(thread)d:打印線程ID。%(threadName)s:打印線程名稱。%(process)d:打印進程ID。%(processName)s:打印線程名稱。%(module)s:打印模塊名稱。%(message)s:打印日志信息。??設(shè)置Formatter主要包括兩種方式,一種是通過Formatter類構(gòu)建Formatter實例,并將其綁定到特定的handler上;一種是通過logging.basicConfig設(shè)置:
import loging import time# 1 logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(funcName)s - %(message)s',datefmt='%Y/%m/%d %H:%M:%S',style='%' # '%', ‘{‘ or ‘$’) # 2 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s',datefmt='%Y/%m/%d %H:%M:%S',style='%') formatter.converter = time.localtime() formatter.converter = time.gmtime()??datefmt的設(shè)置,請參考time.strftime()
logging config
basicConfig
??使用logging模塊的接口函數(shù)basicConfig可以非常方便的進行基本的配置。其中需要注意兩點
- 該函數(shù)stream 、filename以及handlers這三個參數(shù)是互斥的。
- logging.basicConfig函數(shù)是一個one-off simple configuration facility,只有第一次調(diào)用會有效,并且它的調(diào)用應(yīng)該在任何日志記錄事件之前。
stepByStepConfig
??這種設(shè)計方式條理清晰,但是會但麻煩一點點,配置的邏輯就是:一個logger可以有多個handler;每個handler可以有一個Formatter;handler和logger都需要設(shè)置一個log Level;根據(jù)需要logger和handler都可以添加多個Filter。
import logginglogger = logging.getLogger(__name__) logger.setLevel(level=logging.INFO)handler = logging.FileHandler('output.log', mode='a', encoding=None, delay=False) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s',datefmt='%Y/%m/%d %H:%M:%S',style='%') handler.setFormatter(formatter) handler.setLevel(logging.DEBUG) handler.set_name('output-log') logger.addHandler(handler) try:result = 10 / 0 except:logger.error('Faild to get result', exc_info=True)fileConfig
??通過配置文件來配置logging模塊,這是web應(yīng)用中比較常見的一種設(shè)置方式
import logging import logging.configlogging.config.fileConfig('logging.conf', disable_existing_loggers=True)# create logger logger = logging.getLogger('simpleExample')# 'application' code logger.debug('debug message') logger.info('info message') logger.warning('warn message') logger.error('error message') logger.critical('critical message') [loggers] keys=root,simpleExample[handlers] keys=consoleHandler[formatters] keys=simpleFormatter[logger_root] level=DEBUG # can be one of DEBUG, INFO, WARNING, ERROR, CRITICAL or NOTSET 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=??fileConfig文件的解讀主要基于configparser,它必須包含 [loggers], [handlers] 和 [formatters] section。這是一個比較老的API,不支持配置Filter,估計后面也不會更新了,所以建議大家使用dictConfig。
dictConfig
??通過dictConfig配置即可以通過python代碼構(gòu)建一個dict對象,也可以通過yaml、JSON文件來進行配置。字典中必須傳入的參數(shù)是version,而且目前有效的值也只有1。其他可選的參數(shù)包括以下:
-
formatters
對于formatter,主要搜尋format和datefmt參數(shù),用來構(gòu)造Formatter實例。
-
filters
對于filter,主要搜尋name參數(shù)(默認為空字符串),用來構(gòu)造Formatter實例。
-
handlers
對于handlers,主要包括以下參數(shù),其他的參數(shù)將作為關(guān)鍵字參數(shù)傳遞到handlers的構(gòu)造函數(shù):
- class (mandatory). This is the fully qualified name of the handler class.
- level (optional). The level of the handler.
- formatter (optional). The id of the formatter for this handler.
- filters (optional). A list of ids of the filters for this handler.
-
loggers
對于loggers,主要包括以下參數(shù):
- level (optional). The level of the logger.
- propagate (optional). The propagation setting of the logger.
- filters (optional). A list of ids of the filters for this logger.
- handlers (optional). A list of ids of the handlers for this logger.
-
root
這是給root logger的配置項
-
incremental
是否將此配置文件解釋為現(xiàn)有配置的增量, 默認為False
-
disable_existing_loggers
是否要禁用現(xiàn)有的非 root logger,默認為True
??以下給出一個較為完成的YAML示例,注意體會loggers, handlers, formatters, filters之間的關(guān)聯(lián)性,該配置文件定義了brief和simple兩種formatter;定義了console、file、error三個handler,其中console使用brief formatter,file和error使用simple formatter;main.core logger使用file和error handler,root logger使用console handler:
version: 1 formatters:brief:format: "%(asctime)s - %(message)s"simple:format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" handlers:console:class : logging.StreamHandlerformatter: brieflevel : INFOstream : ext://sys.stdoutfile:class : logging.FileHandlerformatter: simplelevel: DEBUGfilename: debug.logerror:class: logging.handlers.RotatingFileHandlerlevel: ERRORformatter: simplefilename: error.logmaxBytes: 10485760backupCount: 20encoding: utf8 loggers:main.core:level: DEBUGhandlers: [file, error] root:level: DEBUGhandlers: [console]??將上面的configuration setup可以通過以下方法:
import logging import yaml import logging.config import osdef setup_logging(default_path='config.yaml', default_level=logging.INFO):if os.path.exists(default_path):with open(default_path, 'r', encoding='utf-8') as f:config = yaml.load(f)logging.config.dictConfig(config)else:logging.basicConfig(level=default_level)??dictConfig通過將dict數(shù)據(jù)傳遞給dictConfigClass,然后返回對象調(diào)用configure函數(shù)使配置生效。
def dictConfig(config):"""Configure logging using a dictionary."""dictConfigClass(config).configure()User-defined objects
??logging模塊 dictConfig為了支持handlers,filters和formatters的用戶自定義對象,可以通過助記符()指定一個工廠函數(shù)來實例化自定義對象,下面定義的 custom formatter相當于my.package.customFormatterFactory(bar='baz', spam=99.9, answer=42)
formatters:brief:format: '%(message)s'default:format: '%(asctime)s %(levelname)-8s %(name)-15s %(message)s'datefmt: '%Y-%m-%d %H:%M:%S'custom:(): my.package.customFormatterFactorybar: bazspam: 99.9answer: 42Access to external objects
??logging模塊 dictConfig為了支持鏈接外部objects,如sys.stdout,可以使用ext://sys.stderr。內(nèi)部原理是進行正則匹配^(?P<prefix>[a-z]+)://(?P<suffix>.*)$,如果prefix有意義,則會按照prefix預(yù)定義的方式處理suffix;否則保持字符串原樣。
Access to internal objects
??logging模塊 dictConfig為了支持鏈接配置文件內(nèi)部objects,首先,比如logger或則handler中的level值設(shè)置DEBUG,配置系統(tǒng)會自動地將其轉(zhuǎn)換成logging.DEBUG;但是,對于logging模塊不知道的用戶定義對象,需要一種更通用的機制來完成,比如我自定義一個handler,這個handler又需要另一個handler來代理,那么可以這樣定義:
handlers:file:# configuration of file handler goes herecustom:(): my.package.MyHandleralternate: cfg://handlers.file??舉一個更復雜的例子,如下面文件所定義YAML配置文件,則cfg://handlers.email.toaddrs[0]會被解析到其值為support_team@domain.tld;subject的值可以通過cfg://handlers.email.subject或者cfg://handlers.email[subject]拿到。
handlers:email:class: logging.handlers.SMTPHandlermailhost: localhostfromaddr: my_app@domain.tldtoaddrs:- support_team@domain.tld- dev_team@domain.tldsubject: Houston, we have a problem.Optimization
??當你不想收集以下信息時,你可以對你的日志記錄系統(tǒng)進行一定的優(yōu)化:
| 有關(guān)調(diào)用來源的信息 | 將 logging._srcfile 設(shè)置為 None 。這避免了調(diào)用 sys._getframe(),如果 PyPy 支持 Python 3.x ,這可能有助于加速 PyPy (無法加速使用了sys._getframe()的代碼)等環(huán)境中的代碼. |
| 線程信息 | 將 logging.logThreads 置為 0 。 |
| 進程信息 | 將 logging.logProcesses 置為 0 。 |
??另外,核心的logging模塊只包含基本的handlers,如果你不顯式導入 logging.handlers 和 logging.config ,它們將不會占用任何內(nèi)存。
實例運用
??logging模塊的落腳點當然是實際項目中的運用,比如對于簡單的程序可以參考以下的使用方式,先在一個模塊中創(chuàng)建并定義好root logger,在其他模塊中調(diào)用get_logger函數(shù)創(chuàng)建其子logger。
import logginglogger = logging.getLogger('logger') logger.propagate = False # wii not pass log messages on to logging.root and its handler logger.setLevel('INFO') logger.addHandler(logging.StreamHandler()) # Logs go to stderr logger.handlers[-1].setFormatter(logging.Formatter('%(message)s')) logger.handlers[-1].setLevel('INFO')def get_logger(name):"""Creates a child logger that delegates to anndata_logger instead to logging.root"""return logger.manager.getLogger(name)??對于更加復雜的場景,用戶也可以重載logging模塊提供的logger類logging.Logger或者logging.RootLogger的自定義logging Level,實現(xiàn)更加靈活的日志記錄功能。比如,以下定義一個新的HINT log Level,通過繼承重寫logging.RootLogger獲得自定義的Logger類。
import loggingHINT = (INFO + DEBUG) // 2 logging.addLevelName(HINT, 'HINT')class RootLogger(logging.RootLogger):def __init__(self, level):super().__init__(level)self.propagate = FalseRootLogger.manager = logging.Manager(self)def log(self,level: int,msg: str,*,extra: Optional[dict] = None,time: datetime = None,deep: Optional[str] = None,) -> datetime:now = datetime.now(timezone.utc)time_passed: timedelta = None if time is None else now - timeextra = {**(extra or {}),'deep': deep,'time_passed': time_passed}super().log(level, msg, extra=extra)return nowdef critical(self, msg, *, time=None, deep=None, extra=None) -> datetime:return self.log(CRITICAL, msg, time=time, deep=deep, extra=extra)def error(self, msg, *, time=None, deep=None, extra=None) -> datetime:return self.log(ERROR, msg, time=time, deep=deep, extra=extra)def warning(self, msg, *, time=None, deep=None, extra=None) -> datetime:return self.log(WARNING, msg, time=time, deep=deep, extra=extra)def info(self, msg, *, time=None, deep=None, extra=None) -> datetime:return self.log(INFO, msg, time=time, deep=deep, extra=extra)def hint(self, msg, *, time=None, deep=None, extra=None) -> datetime:return self.log(HINT, msg, time=time, deep=deep, extra=extra)def debug(self, msg, *, time=None, deep=None, extra=None) -> datetime:return self.log(DEBUG, msg, time=time, deep=deep, extra=extra)寫在篇后
??本篇博客基本上涵蓋了python logging模塊大部分的功能,但是也有一些尚未cover。比如logging模塊會默認吞噬除了SystemExit和KeyboardInterrupt的一切異常,因為logging.raiseExceptions默認為True(生產(chǎn)環(huán)境也推薦設(shè)置為True);logging.captureWarnings(capture=True)會重定向warning信息到logging模塊;另外,可以通過logging.setLoggerClass()決定初始化logger的類型,與之對應(yīng)的有l(wèi)ogging.getLoggerClass()
等等,更多的用法在實踐中再慢慢總結(jié)經(jīng)驗,吸取教訓。
總結(jié)
以上是生活随笔為你收集整理的python Logging日志记录模块详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python Typing模块-类型注解
- 下一篇: 万字长文深度解析python 单元测试