Python中logging模块
在项目中我们常常需要打印日志,特别是在系统级项目上一般都会有自己日志模块,下面我们将介绍下Python中自带的logging模块(注意这是模块的名称并不是类)
一、基本使用
logging是一个包的名称,我们真正使用的是logging.Logger这个类。但是我们不能使用常规的方式进行初始化,而是使用给logging.getLogger("名字")的方式获得。比如以下代码:
logger = logging.getLogger("myLogger")
logger_sub = logging.getLogger("myLogger.sub") # 这种方式可以得到上一行logger的子logger
需要注意的是logging模块通过“名字”来获取logging.Logger对象,因此只要“名字”是一样的,得到的就是同一个logger对象(logging类似维护了一个logger和其名字的哈希表),比如以下代码返回的就是True。
logger1 = logging.getLogger("myLogger")
logger2 = logging.getLogger("myLogger")
print(id(logger1) == id(logger2))
我们拿到了logger对象之后就可以使用.debug(),.info(),.error()等方法来输出了,比如以下代码:
logger = logging.getLogger("myLogger")
# 设置记录级别
logger.setLevel(logging.DEBUG)
logger.debug("debug信息")
logger.info("info信息")
当你运行后可以发现,诶~怎么没有输出呢?那是因为我们还没有给logger增加handler(处理器),可以理解我们给logger传入了信息(比如这里的字符串),但是logger并不知道拿到这个信息该干嘛,我们最通常的期望就是,这个logger可以把我们的信息输出到控制台或者文件中。为了满足这个期待我们就需要做以下工作:
1、创建一个handler处理器
2、为logger绑定这个handler处理器(一个logger对象可以绑定多个handler处理器,当然也可以移除)
一般情况下我们只需要使用logging包中已经为我们准备好的handler就可以了,一般常用的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服务器
了解到了以上之后,我们们可以对上述示例代码进行修改:
import logging
logger = logging.getLogger("myLogger")
# 设置记录级别
logger.setLevel(logging.DEBUG)
# 设置handler并绑定
handler = logging.StreamHandler()
logger.addHandler(handler)
logger.debug("debug信息")
logger.info("info信息")
可以在控制台中得到输出结果:
或许你觉得为什么这跟print()打印的是一样的呢?为什么没有那些什么时间,日志等级等信息呢?其实这些东西只需要对handler进行配置输出格式的配置即可(注意是对handler进行输出格式的配置,而不是对logger对象)。我们可以采用以下格式模板,对我们的日志输出添加额外的输出:
属性名称
|
格式
|
说明
|
name
|
%(name)s
|
日志的名称
|
asctime | %(asctime)s | 可读时间,默认格式‘2003-07-08 16:49:45,896',逗号之后是毫秒 |
filename | %(filename)s | 当前文件名 |
pathname | %(pathname)s | 文件的全路径名称 |
funcName | %(funcName)s | 调用日志多对应的方法名 |
levelname | %(levelname)s | 日志的等级(低到高的顺序是: DEBUG < INFO < WARNING < ERROR < CRITICAL) |
levelno | %(levelno)s | 日志等级的数字表示 |
lineno | %(lineno)d | 被记录日志在源码中的行数 |
module | %(module)s | 模块名 |
process | %(process)d | 进程的ID |
processName | %(processName)s | 进程的名称 |
thread | %(thread)d | 线程的ID |
threadName | %(threadName)s | 线程的名称 |
relativeCreated | %(relativeCreated)d | 日志被创建的相对时间,以毫秒为单位 |
基于以上模板格式,我们可以进一步修改代码:
import logging
logger = logging.getLogger("myLogger")
# 设置记录级别
logger.setLevel(logging.DEBUG)
# 创建handler
handler = logging.StreamHandler()
# 为handler设置格式
handler.setFormatter(logging.Formatter("%(filename)s,行%(lineno)s \t %(levelname)s:%(message)s"))
# 绑定handler到logger
logger.addHandler(handler)
logger.debug("debug信息")
logger.info("info信息")
上述代码的第9行中,我们使用handler.setFormatter()方法配置了一个输出格式,这里需要注意该方法接受的是一个logging.Formatter对象,因此我们这里创建了一个logging.Formatter对象(创建这个对象时就可以把我们的格式化输出结构传递进去,比如这里我们让日志打印的格式为: 当前文件名,行 日志等级,需要打印的日志信息),最终结果如下:
二、多模块的使用
还记得我们在文章中一开始的创建logger的示例代码中,使用了xxx.xxx的方式创建了子logger吗?是的,logger对象我们也存在父子关系,比如下面代码中的logger就是logger_sub的父类对象(在logging中我们根据创建时传递的logger名字,来得到父子关系),“myLogger”是第一级logger对象,"myLogger.sub"就是第二级logger对象,同样的“myLogger.sub.son”就是第三级logger对象。
logger = logging.getLogger("myLogger")
logger_sub = logging.getLogger("myLogger.sub")
对于存在父子级关系的logger对象有什么作用和意义呢?子级logger对象既可以使用绑定在自己身上的handler,也会将信息传递给父级logger对象,从而触发父级的handler处理。比如下面的例子
# 创建父类logger
logger = logging.getLogger("myLogger")
# 设置父类logger的日志打印等级
logger.setLevel(logging.ERROR)
# 设置父类logger的handler
father_handler = logging.StreamHandler()
father_handler.setFormatter(logging.Formatter("父类格式: %(filename)s,line %(lineno)s \t %(levelname)s:%(message)s"))
logger.addHandler(father_handler)
# 父类logger打印
logger.error("父类logger日志打印")
# 创建子类logger
logger_sub = logging.getLogger("myLogger.sub")
logger_sub.error("这是子类logger, 继承了父类的配置")
前面10行代码就是我们之前的代码,我们重点关注最后两行。最后两行代码中我们创建了一个logger对象并且并没有为其绑定任何的handler,按照之前的道理他本不应该打印出任何的东西,但由于这个logger是“myLogger”的子logger对象,因此子类传入的日志信息"这是子类logger, 继承了父类的配置"会被顺势传递到父类logger中由父类logger绑定的handler处理。因此上述代码的运行结果如下:
大家自行尝试下如果把最后一行代码改为 logger_sub.debug("这是子类logger, 继承了父类的配置") 会得到什么样的打印结果。
下面我们继续讨论,如果我给子logger_sub也绑定他自己的handler会怎么呢:
# 创建父类logger
logger = logging.getLogger("myLogger")
# 设置父类logger的日志打印等级
logger.setLevel(logging.ERROR)
# 设置父类logger的handler
father_handler = logging.StreamHandler()
father_handler.setFormatter(logging.Formatter("父类格式: %(filename)s,line %(lineno)s \t %(levelname)s:%(message)s"))
logger.addHandler(father_handler)
# 父类logger打印
logger.error("父类logger日志打印")
print("=============================================")
# 创建子类logger
logger_sub = logging.getLogger("myLogger.sub")
logger_sub.setLevel(logging.DEBUG)
# 配置子类handler
sub_handler = logging.StreamHandler()
sub_handler.setFormatter(logging.Formatter("子类格式: %(message)s"))
logger_sub.addHandler(sub_handler)
# 打印子类logger
logger_sub.debug("这是子类logger")
在代码的后半段,我们以同样的方式创建了子类logger,并且也为这个子类logger设置了自己handler(注意这个handler的打印格式与父类的不一致哟),下面的运行结果:
从运行结果上,我们会很自然的有两个问题?为什么子类logger的信息被父类的handler处理的,并且是如何突破父类的日志等级过滤的(因为父类的日志等级设定的是ERROR,按道理子类的DEBUG等级的信息无法被父类handler处理)。我们从以下两张图中可以得到结果
默认情况下,子logger中收到的一条信息,那么子系统处理完后会将这条信息传递给父亲logger,需要注意的是如果一个信息被logger的日志等级过滤了,那么这条信息将不再向上传递(比如子logger设置的日志等级为Error,一条子logger生成的debug信息将不会被传递给父logger)。至于第二个问题“如何突破父类的日志等级过滤”,我猜想是因为实际上子类是继承了父类的handler,看起来子类有一个handler,但实际上子类的每条信息都需要经过两个handler,因此只要子类信息能到达handler处理,就会被绑定在父类的handler处理。为了避免无法理解的日志打印,建议使用子类logger时关闭记录的传递( logger_sub.propagate = False )
三、自定义Handler
我们可以自定义自己的Handler,只需要继承logging.Handler即可。以下是示例代码:
class MyHandler(logging.Handler):
def __init__(self, something):
# 调用父类初始化方法
super().__init__()
# 可以传入一个函数
self.something = something
def emit(self, record):
# 这是Handler的核心方法,每当有新的日志记录需要处理时,emit方法会被调用
log_entry = self.format(record) # 格式化日志记录
try:
# 将格式化后的log_entry发送到somthing函数中处理
self.something(log_entry)
except Exception:
# 发送失败时,可以捕获异常并记录错误
self.handleError(record)
四、使用案例以及技巧
1、打印error时可以指定打印traceback
logger.error("错误信息", exc_info=True) 这样的将其包裹在try-except中,即可在触发异常时自动获取并记录最近一次未被捕获的异常的相关信息。
参考链接
Python中logger日志模块详解_python logger-CSDN博客
如何设置现代 Python 日志记录 | Python 基础教程 | Python 冷知识 | 十分钟高手系列_哔哩哔哩_bilibili