参考:
python中logging模块上篇
Python Logging 模块完全解读
django日志logging的配置以及处理
python Logging日志记录模块详解
Python 中的 logging 模块可以让你跟踪代码运行时的事件,当程序崩溃时可以查看日志并且发现是什么引发了错误。Log 信息有内置的层级——调试(debugging)、信息(informational)、警告(warnings)、错误(error)和严重错误(critical)。你也可以在 logging 中包含 traceback 信息。不管是小项目还是大项目,都推荐在 Python 程序中使用 logging。
当你运行一个 Python 脚本时,你可能想要知道脚本的哪个部分在执行,并且检视变量的当前值。
通常,可以只使用print()打印出你想要的信息。在小程序中,可能靠这个就足够了。
但问题是,当你处理有很多个模块的大项目时,就需要一个更加灵活的方法。
因为代码需要经历开发、调试、审查、测试或者上线等不同阶段。在开发时你想要打印的信息类型可能和上线后你想看到的信息类型完全不同。
也就是说,在“测试”时,你可能只想看警告和错误信息,然而在“调试”时,你可能还想看到跟调试相关的信息。
如果你还想打印出使用的模块以及代码运行的时间,那么你的代码很容易变得混乱。
使用logging模块,这些问题就能很容易地解决。
控制信息层级,仅记录需要的信息。
控制显示或者保存日志的时机。
使用内置信息模板控制日志格式。
知晓信息来自于哪个模块。
logging模块是 Python 的标准库,要使用 logging,只需要使用logging.basicConfig()
进行基本设置。事实上,这也是可选的。然后就可以调用logging.{level}(message)
在控制台中显示信息。
import logging
logging.basicConfig(level=logging.INFO)
logging.info('this is a info')
日志级别:日志器名称:日志内容
{level}:{logger}:{message}
logging模块提供的日志记录函数所使用的日志器设置的日志格式默认是BASIC_FORMAT,其值为:
"%(levelname)s:%(name)s:%(message)s"
上面的例子中,level就是INFO,因为调用的是logging.info()。logger为root,因为这是默认 logger。输出的信息message就是我传递给logging.info()的字符串。
上面例子中,如果不设置logging.basicConfig(level=logging.INFO)的话,日志信息不会被打印出来,默认 logger 是root,其默认的 basicConfig 级别是WARNING。也就是说,只有来自logging.warning或者更高级别的信息才会被记录下来。因此,logging.info()中的信息不会被打印出来。这也是为什么 basicConfig 被设为INFO。
测试如下
import logging
logging.debug("This is a debug log.")
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")
执行结果为
默认 logger 是root,其默认的 basicConfig 级别是WARNING,因此只有WARNING级别的日志记录以及大于它的ERROR和CRITICAL级别的日志记录被输出了,而小于它的DEBUG和INFO级别的日志记录被丢弃了。
级别 | 值 | 描述 |
---|---|---|
CRITICAL/FATAL | 50 | 关键错误/消息 |
ERROR | 40 | 错误 |
WARNING | 30 | 警告消息 |
INFO | 20 | 通知消息 |
DEBUG | 10 | 调试 |
NOTSET | 0 | 无级别 |
如果设置日志级别为WARNING,则通过日志记录器则输入WARNING级别以下的信息是不会被处理的。
开发应用程序或部署开发环境时,可以使用DEBUG或INFO级别的日志获取尽可能详细的日志信息来进行开发或部署调试;应用上线或部署生产环境时,应该使用WARNING或ERROR或CRITICAL级别的日志来降低机器的I/O压力和提高获取错误日志信息的效率。日志级别的指定通常都是在应用程序的配置文件中进行指定的。
字段/属性名称 | 使用格式 | 描述 |
---|---|---|
asctime | %(asctime)s | 日志事件发生的时间–人类可读时间,如:2003-07-08 16:49:45,896 |
created | %(created)f | 日志事件发生的时间–时间戳,就是当时调用time.time()函数返回的值 |
relativeCreated | %(relativeCreated)d | 日志事件发生的时间相对于logging模块加载时间的相对毫秒数(目前还不知道干嘛用的) |
msecs | %(msecs)d | 日志事件发生事件的毫秒部分 |
levelname | %(levelname)s | 该日志记录的文字形式的日志级别(‘DEBUG’, ‘INFO’, ‘WARNING’, ‘ERROR’, ‘CRITICAL’) |
levelno | %(levelno)s | 该日志记录的数字形式的日志级别(10, 20, 30, 40, 50) |
name | %(name)s | 所使用的日志器名称,默认是’root’,因为默认使用的是 rootLogger |
message | %(message)s | 日志记录的文本内容,通过 msg % args计算得到的 |
pathname | %(pathname)s | 调用日志记录函数的源码文件的全路径 |
filename | %(filename)s | pathname的文件名部分,包含文件后缀 |
module | %(module)s | filename的名称部分,不包含后缀 |
lineno | %(lineno)d | 调用日志记录函数的源代码所在的行号 |
funcName | %(funcName)s | 调用日志记录函数的函数名 |
process | %(process)d | 进程ID |
processName | %(processName)s | 进程名称,Python 3.1新增 |
thread | %(thread)d | 线程ID |
threadName | %(thread)s | 线程名称 |
import logging
#设置日志格式
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
logging.info('this is a info')
执行结果为
例如这里将年月日中间改为 /
import logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(message)s',
datefmt='%Y/%m/%d %H:%M:%S')
logging.info('this is a info')
import logging
logging.basicConfig(level=logging.INFO,
format='%(name)s %(levelname)s %(asctime)s %(message)s')
logger = logging.getLogger('mylogger')
logger.info('this is a info')
通过getLogger指定logger的名称,名称可以在format中用%(name)s格式化输出。
指定logger名称有一个好处是,在同一个程序的不同文件中引用相同名称的logger对应的是同一个实例,这有利于logging的跨文件调用。
在介绍logging模块的日志流处理流程之前,我们先来介绍下logging模块的四大组件:
组件名称 | 对应类名 | 功能描述 |
---|---|---|
日志器 | Logger | 提供了应用程序可一直使用的接口 |
处理器 | Handler | 将logger创建的日志记录发送到合适的目的输出 |
过滤器 | Filter | 提供了更细粒度的控制工具来决定输出哪条日志记录,丢弃哪条日志记录 |
格式器 | Formatter | 决定日志记录的最终输出格式 |
logging模块就是通过这些组件来完成日志处理的,上面所使用的logging模块级别的函数也是通过这些组件对应的类来实现的。
简单点说就是:日志器(logger)是入口,真正干活儿的是处理器(handler),处理器(handler)还可以通过过滤器(filter)和格式器(formatter)对要输出的日志内容做过滤和格式化等处理操作。
方法 | 描述 |
---|---|
Logger.setLevel() | 设置日志器将会处理的日志消息的最低严重级别 |
Logger.addHandler() 和 Logger.removeHandler() | 为该logger对象添加 和 移除一个handler对象 |
Logger.addFilter() 和 Logger.removeFilter() | 为该logger对象添加 和 移除一个filter对象 |
内建等级中,级别最低的是DEBUG,级别最高的是CRITICAL。例如setLevel(logging.INFO),此时函数参数为INFO,那么该logger将只会处理INFO、WARNING、ERROR和CRITICAL级别的日志,而DEBUG级别的消息将会被忽略/丢弃。
方法 | 描述 |
---|---|
Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), Logger.critical() | 创建一个与它们的方法名对应等级的日志记录 |
Logger.exception() | 创建一个类似于Logger.error()的日志消息 |
Logger.log() | 需要获取一个明确的日志level参数来创建一个日志记录 |
说明:
logging.getLogger()方法有一个可选参数name,该参数表示将要返回的日志器的名称标识,如果不提供该参数,则其值为’root’。若以相同的name参数值多次调用getLogger()方法,将会返回指向同一个logger对象的引用。
Handler对象的作用是(基于日志消息的level)将消息分发到handler指定的位置(文件、网络、邮件等)。Logger对象可以通过addHandler()方法为自己添加0个或者更多个handler对象。比如,一个应用程序可能想要实现以下几个日志需求:
这种场景就需要3个不同的handlers,每个handler复杂发送一个特定严重级别的日志到一个特定的位置。
一个handler中只有非常少数的方法是需要应用开发人员去关心的。对于使用内建handler对象的应用开发人员来说,似乎唯一相关的handler方法就是下面这几个配置方法:
方法 | 描述 |
---|---|
Handler.setLevel() | 设置handler将会处理的日志消息的最低严重级别 |
Handler.setFormatter() | 为handler设置一个格式器对象 |
Handler.addFilter() 和 Handler.removeFilter() | 为handler添加 和 删除一个过滤器对象 |
需要说明的是,应用程序代码不应该直接实例化和使用Handler实例。因为Handler是一个基类,它只定义了素有handlers都应该有的接口,同时提供了一些子类可以直接使用或覆盖的默认行为。下面是一些常用的Handler,可以使用下面的方法来创建handler实例,并且设置输出位置等参数,然后将其添加到日志器中,在下一节代码中有体现:
Handler | 描述 |
---|---|
logging.StreamHandler | 将日志消息发送到输出到Stream,如std.out, std.err或任何file-like对象。 |
logging.FileHandler | 将日志消息发送到磁盘文件,默认情况下文件大小会无限增长 |
logging.handlers.RotatingFileHandler | 将日志消息发送到磁盘文件,并支持日志文件按大小切割 |
logging.hanlders.TimedRotatingFileHandler | 将日志消息发送到磁盘文件,并支持日志文件按时间切割 |
logging.handlers.HTTPHandler | 将日志消息以GET或POST的方式发送给一个HTTP服务器 |
logging.handlers.SMTPHandler | 将日志消息发送给一个指定的email地址 |
logging.NullHandler | 该Handler实例会忽略error messages,通常被想使用logging的library开发者使用来避免’No handlers could be found for logger XXX’信息的出现。 |
PS:这种方法我在windows环境下django中试了不是好用,可能会出现PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问
这样的报错。因为Django默认启用两个进程,一个进程用来检测文件变化,另一个进程是正经的服务器进程。settting.py这个文件被加载了两次,也就是日志文件打开了两次,如果是服务器进程先启动,则看不出毛病来;如果是监控进程首先打开了从settings.py加载了日志,那么正经服务器进程就无法再次加载日志了。 参考:https://www.qedev.com/python/3333.html
如果你的Django项目是单进程的,那么在启动Django项目的时候使用–noreload就可以解决
python manage.py runserver 0.0.0.0:80 --noreload
noreload表示不启动Django的监控进程,也就是说项目代码的改变不再会影响已经载入内存中的代码,这样,Django就只会起一个进程。
如果是多进程的,或者在某些单独的模块,比如celery中为了使用Django的ORM而使用了django.setup()
对于TimedRotatingFileHandler没找到很好的解决方法。。
TimedRotatingFileHandler 可以理解为是一种可设置固定时间间隔的日志记录类,它被集成在 logging 中,直接调用进行实例化和配置就可以使用
TimedRotatingFileHandler 的构造函数为:TimedRotatingFileHandler( filename [, when [, interval [, backupCount] ] ] )
参数解释:
filename 是输出日志的文件名称前缀,比如说 testServiceLog 这样的就是日志文件名前缀
when 是一个字符串,定义了日志切分的间隔时间单位,这是一个枚举类,可选参数如下:
"S":Second 秒
"M":Minutes 分钟
"H":Hour 小时
"D":Days 天
"W":Week day(0 = Monday) (interval==0时代表星期一)
"midnight":Roll over at midnight 每天的凌晨
代码例子如下:
每天生成一个日志文件,保留最近7天的日志文件,会根据日期来生成文件名称
log_file_handler = TimedRotatingFileHandler(filename=logpath, when="D", interval=1, backupCount=7)
代码示例
log_file_handler = TimedRotatingFileHandler(filename=logpath,maxBytes=1024*1024, backupCount=5)
举个例子,如果backupCount=5,log file定义的名字为app.log,你会得到app.log, app.log.1, app.log.2 一直到 app.log.5。
然而被写入日志的永远是app.log,写满了之后重命名为app.log.1,如果app.log.1存在,app.log.1会先被重名名为app.log.2,依此类推。
PS:同样会遇到多进程使用文件的问题
logging.handlers.RotatingFileHandler
这种方法在django中也不好用,也会出现PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问
这个错误,可以跟上面一样启动django时加上--noreload
python manage.py runserver 0.0.0.0:80 --noreload
但也可以改为使用concurrent_log_handler.ConcurrentRotatingFileHandler
,这个处理器对多进程的时候同样管用。
参考:https://www.codeleading.com/article/77632670363/
https://www.qedev.com/python/3333.html
例子如下
from logging import getLogger, INFO
from concurrent_log_handler import ConcurrentRotatingFileHandler
import os
log = getLogger()
# Use an absolute path to prevent file rotation trouble.
logfile = os.path.abspath("mylogfile.log")
# Rotate log after reaching 512K, keep 5 old copies.
rotateHandler = ConcurrentRotatingFileHandler(logfile, "a", 512*1024, 5)
log.addHandler(rotateHandler)
log.setLevel(INFO)
log.info("Here is a very exciting log message, just for you")
Formater对象用于配置日志信息的最终顺序、结构和内容。与logging.Handler基类不同的是,应用代码可以直接实例化Formatter类。另外,如果你的应用程序需要一些特殊的处理行为,也可以实现一个Formatter的子类来完成。
Formatter类的构造方法定义如下:
logging.Formatter.__init__(fmt=None, datefmt=None, style='%')
可见,该构造方法接收3个可选参数:
设置Formatter主要包括两种方式,一种是通过Formatter类构建Formatter实例,并将其绑定到特定的handler上;一种是通过logging.basicConfig设置
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()
Filter可以被Handler和Logger用来做比level更细粒度的、更复杂的过滤功能。Filter是一个过滤器基类,它只允许某个logger层级下的日志事件通过过滤。该类定义如下:
class logging.Filter(name='')
filter(record)
比如,一个filter实例化时传递的name参数值为’A.B’,那么该filter实例将只允许名称为类似如下规则的loggers产生的日志记录通过过滤:‘A.B’,‘A.B,C’,‘A.B.C.D’,‘A.B.D’,而名称为’A.BB’, 'B.A.B’的loggers产生的日志则会被过滤掉。如果name的值为空字符串,则允许所有的日志事件通过过滤。
filter方法用于具体控制传递的record记录是否能通过过滤,如果该方法返回值为0表示不能通过过滤,返回值为非0表示可以通过过滤。
logging标准库只提供了一个base Filter,其构造函数__init__接收name参数,默认的行为是名为name的logger极其子logger的消息能通过过滤,其余皆会被滤掉。当然我们也可以根据具体的业务逻辑自定义Filter,并且也非常简单,只需要继承logging.Filter类重写filter方法即可,filter方法接收record对象作为参数,返回True代表通过过滤,返回False表示该record被过滤
import logging
logger = 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 False
return True
handler.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也可以不继承logging.Filter,只需要定义一个函数并同样绑定即可:
def _filter(record):
if 'result' in record.msg:
return False
return True
handler.addFilter(_filter)
说明:
可见,一条日志信息要想被最终输出需要依次经过以下几次过滤:
需要说明的是: 关于上面第9个步骤,如果propagate值为1,那么日志消息会直接传递交给上一级logger的handlers进行处理,此时上一级logger的日志等级并不会对该日志消息进行等级过滤。
现在有以下几个日志记录的需求:
import logging
import logging.handlers
import datetime
#实例化一个日志器对象
logger = logging.getLogger('mylogger')
#设置日志器处理日志的最低基本为DEBUG
logger.setLevel(logging.DEBUG)
# 是否传播这个日志到祖先logger, 如果设置了False 就不会传到root logger(祖先Logger)的
# 默认StreamHandler那里, 也就是不会打印在页面上
logger.propagate = False
#创建一个TimedRotatingFileHandler类型的handler处理器,因为需要按照时间进行日志切割,设置写入到all.log这个文件
rf_handler = logging.handlers.TimedRotatingFileHandler('all.log', when='midnight', interval=1, backupCount=7,
atTime=datetime.time(0, 0, 0, 0))
#实例化一个格式器对象
rf_formatter=logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
#为handler处理器设置一个格式器对象
rf_handler.setFormatter(rf_formatter)
#创建一个FileHandler类型的handler处理器,设置写入到error.log这个文件
f_handler = logging.FileHandler('error.log')
#为hanlder设置处理的日志消息的最低严重级别为ERROR
f_handler.setLevel(logging.ERROR)
#实例化一个格式器对象
rf_formatter=logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s")
#为handler处理器设置一个格式器对象
f_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s"))
#为该logger对象添加handler处理器对象
logger.addHandler(rf_handler)
logger.addHandler(f_handler)
#输出相关日志
logger.debug('debug message')
logger.info('info message')
logger.warning('warning message')
logger.error('error message')
logger.critical('critical message')
all.log文件输出
2020-07-15 15:21:02,086 - DEBUG - debug message
2020-07-15 15:21:02,086 - INFO - info message
2020-07-15 15:21:02,086 - WARNING - warning message
2020-07-15 15:21:02,086 - ERROR - error message
2020-07-15 15:21:02,086 - CRITICAL - critical message
error.log文件输出
2020-07-15 15:21:02,086 - ERROR - loggingtest.py[:62] - error message
2020-07-15 15:21:02,086 - CRITICAL - loggingtest.py[:63] - critical message
使用logging模块的接口函数basicConfig可以非常方便的进行基本的配置。其中需要注意两点
import logging
logging.basicConfig(
filename='./log.log',
filemode='w',
format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(funcName)s - %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
style='%',
level=logging.DEBUG
)
logger = logging.getLogger(__name__)
try:
result = 10 / 0
except:
logger.error('Faild to get result', exc_info=True)
logger.log(50, 'logging critical test')
这种设计方式条理清晰,但是会但麻烦一点点,配置的逻辑就是:一个logger
可以有多个handler
;每个handler
可以有一个Formatter
;handler
和logger
都需要设置一个log Level
;根据需要logger
和handler
都可以添加多个Filter。
import logging
logger = 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)
通过配置文件来配置logging模块,这是web应用中比较常见的一种设置方式
fileConfig文件的解读主要基于configparser,它必须包含 [loggers], [handlers] 和 [formatters] section。这是一个比较老的API,不支持配置Filter,估计后面也不会更新了,所以建议大家使用dictConfig。
import logging
import logging.config
logging.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=
通过dictConfig配置即可以通过python代码构建一个dict对象,也可以通过yaml、JSON文件来进行配置。字典中必须传入的参数是version,而且目前有效的值也只有1。其他可选的参数包括以下:
formatters
对于formatter,主要搜寻format和datefmt参数,用来构造Formatter实例。
filters
对于filter,主要搜寻name参数(默认为空字符串),用来构造Formatter实例。
handlers
对于handlers,主要包括以下参数,其他的参数将作为关键字参数传递到handlers的构造函数:
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.
handlers:
console:
class : logging.StreamHandler
formatter: brief
level : INFO
filters: [allow_foo]
stream : ext://sys.stdout
file:
class : logging.handlers.RotatingFileHandler
formatter: precise
filename: logconfig.log
maxBytes: 1024
backupCount: 3
对于loggers,主要包括以下参数:
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 logger的配置项
是否将此配置文件解释为现有配置的增量, 默认为False
是否要禁用现有的非 root logger,默认为True
以下给出一个较为完成的YAML示例,注意体会loggers, handlers, formatters, filters之间的关联性,该配置文件定义了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.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: [file, error]
root:
level: DEBUG
handlers: [console]
将上面的configuration setup可以通过以下方法:
import logging
import yaml
import logging.config
import os
def 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数据传递给dictConfigClass,然后返回对象调用configure函数使配置生效。
def dictConfig(config):
"""Configure logging using a dictionary."""
dictConfigClass(config).configure()
重新开个博客
Django 使用logging生成日志