日志是程序设计及软件开发中不可或缺的重要组成部分,它记录了程序运行时的关键信息,便于开发者进行调试、监控和分析。为什么需要日志?日志记录可以帮助开发人员定位和解决程序中的错误和异常以便快速进行定位问题所在并进行故障排查。生产中通过对日志的分析可以了解服务器的负载、健康状况、客户的分布情况、行为等,无论是开发大型企业应用还是编写小型脚本,算法的部属落地等场景,日志输出都是极其关键的。
python中提供了一个灵活且强大的日志记录logging模块,【当然,它仍然存在一些问题,这个,我们后期详解】,它提供了一套丰富的功能及配置选项,用于在应用程序中记录各种级别的日志消息。logging日志具有以下几个特点:
-
日志级别,根据选择不同的级别来记录、过滤和显示日志信息;
-
允许同时将日志消息发送到多个处理器和输出位置,如文件、控制台、网络等。可以根据需要配置不同的处理器和输出方式;
-
可以通过设置日志消息的格式,包括时间戳、日志级别、文件名、行号等信息,以及自定义的文本信息。可以根据需求自定义日志消息的格式和样式。
-
日志轮转和回滚:logging库支持日志文件的轮转和回滚,可以实现按日期、大小等条件自动切分和管理日志文件;
-
日志配置文件:可以通过配置文件的方式来设置logging库的行为和属性,使得日志记录的配置更加灵活和可配置。
1、选择不同级别来记录日志。设置一个级别,严重程度低于设置值的日志消息将被忽略 | |
日志级别 | 数值 |
CRITICAL | 50 |
ERROR | 40 |
WARNING | 30,默认 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
2、设置日志消息格式 | |
格式 | 说明 |
%(message)s | 日志消息内容 |
%(asctime)s | 创建日志记录可读时间 |
%(funcName)s | 日志调用所在函数名 |
%(levelname)s | 日志级别 |
%(levelno)s | 消息级别数字, |
%(lineno)s | 日志调用所在源码行号 |
%(module)s | 模块(filename名字部分) |
%(process)s | 进程ID |
%(thread)s | 线程ID |
%(processName)s | 进程名 |
%(threadName)s | 线程名 |
1、logging模块
import logging
#如上表参数,配置日志输出样式
logging.basicConfig(format="%(asctime)s-%(filename)s-[line:%(lineno)d]-%(levelname)s-%(thread)d-%(threadName)s [%(message)s]"
,datefmt="%Y:%m:%d %I:%M:%S" #设置日期格式
,level=logging.INFO #设置日志级别
,filename="output.txt" #设置日志输出路径
,filemode="a" #设置文件的打开模式
# ,stream=sys.stderr #如果设置文件输出则此处无效,默认标准错误输出
)
logging.critical('重大故障')
logging.info("my name is ywxk")
logging.warning("这有点不对劲")
logging.error("这是一个Error")
logging.debug("这是一个大Bug")
2、logger对象
logging模块加载的时候,会创建一个root logger。跟logger对象的默认级别是WARNING。可调用logging.baseicConfig来调整级别,就是对这个根Logger的级别进行调整。
import logging
format = "%(asctime)-15s\t %(message)s"
logging.basicConfig(format=format,level=logging.INFO)
logger = logging.getLogger(__name__)
print(logger.getEffectiveLevel())
print(logger.name,type(logger),logger.parent,id(logger))
logger.info("hello1")
logger.warning("warning 1")
logger.error("error 1")
logger.critical("critical 1")
"""
没有输出"hello1" 消息level 20 < effectiveLevel:28
2023-10-06 12:52:00,826 __main__ warning 1
2023-10-06 12:52:00,828 __main__ error 1
2023-10-06 12:52:00,829 __main__ critical 1
EffectiveLevel:28
"""
# 重新设置消息级别
logger.setLevel(20)
print(logger.getEffectiveLevel())
logger.info("hello2")
logger.warning("hello2 warning")
root = logging.getLogger()
root.info("hello2 info root")
"""
有输出"hello2" 消息level 20 >= effectiveLevel:20
2023-10-06 13:01:31,305 __main__ hello2
2023-10-06 13:01:31,307 __main__ hello2 warning
2023-10-06 13:01:31,308 root hello4 info root
20
"""
2.1 日志流-level的继承
logger是层次结构的,使用.号分割,如:a.b,a.b.c或者a.b.c.d。a.b就是a的子代。
import logging
format = "%(asctime)s %(name)s %(message)s"
logging.basicConfig(format=format,level=logging.INFO)
root = logging.getLogger()
print(root.name,type(root),id(root.parent),id(root))
log1 = logging.getLogger("s")
print(log1.name,type(log1),id(log1.parent),id(log1))
log1.setLevel(logging.WARNING)
log2 = logging.getLogger("s.s1")
print(log2.name,type(log2),id(log2.parent),id(log2))
log2.error("log2 waring")
"""
log2实例,如果设置level就用它和信息的级别比较,否则继承最近父类的level,如果父logger的level没有设置,就继续往上找,直到root,如果root没有设置就使用默认的级别WARNING
2023-10-06 13:15:43,878 s.s1 log2 waring
root <class 'logging.RootLogger'> 140725962095832 2381787622512
s <class 'logging.Logger'> 2381787622512 2381856202176
s.s1 <class 'logging.Logger'> 2381856202176 2381856201696
"""
2.2 日志流-信息传递流程
-
如果消息在某一个logger对象上产生,这个logger就是当前logger,消息level首先要和当前level的EffictiveLevel比较,如果低于则流程结束,否则生成log记录;
-
日志记录会交给当前logger的所有handler处理,记录还要和每一个handler的级别分别比较,低的不处理,否则按照handler输出日志记录;
-
当前logger的所有handler处理完后,就要看自己的propagate属性,如果True表示向父logger传递这个日志记录,否则到此流程结束;
-
如果日志记录传递到了父logger,不需要和logger的level进行比较,而是直接交给父的所有handler,父logger成为当前level。重复2,3步骤,直到当前loggerd的父logger是None退出,也就是说当前logger最后一般是root logger(是否能到root logger要看中间的logger是否允许propagate,实例初始的propagate属性为True,即允许向父logger传递消息)
3、Handler对象
handler日志记录中真正干活的。handler控制日志信息的输出目的地,可以是控制台、文件。可以单独设置level,格式及过滤器。如果在basicConfig中设置了文件名则为根logger加一个输出到文件的Handler;如果没有设置文件名,则为根加一个StreamHandler默认输出到sys.stderr。
4、 Formatters对象
Formatters对象配置了日志消息内容的输出结构形式。
logging.Formatter.__init__(
fmt=None, # fmt:消息格式
datefmt=None, # datefmt:时间格式,
style='%' # datefmt:时间格式,默认为:%Y-%m-%d %H:%M:%S
)
4.1 配置日志程序的三种形式-01-同时输出日志到标准输出和文件案例:
import logging
# 1、创建一个logger
logger = logging.getLogger('mylogger')
logger.setLevel(logging.DEBUG)
# 2、创建一个handler,用于写入日志文件
fh = logging.FileHandler('test.log')
fh.setLevel(logging.DEBUG)
# 再创建一个handler,用于输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# 3、定义handler的输出格式(formatter)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 4、给handler添加formatter
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 5、给logger添加handler
logger.addHandler(fh)
logger.addHandler(ch)
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
"""
2023-10-06 14:14:29,054 - mylogger - DEBUG - debug message
2023-10-06 14:14:29,054 - mylogger - INFO - info message
2023-10-06 14:14:29,054 - mylogger - WARNING - warn message
2023-10-06 14:14:29,054 - mylogger - ERROR - error message
2023-10-06 14:14:29,054 - mylogger - CRITICAL - critical message
"""
4.2 配置日志程序的三种形式-02-创建日志配置文件并使用fileConfig()函数读取它
[loggers]
keys=root,example
[logger_root]
level=DEBUG
handlers=hand01
[logger_example]
handlers=hand02,hand03
qualname=example
propagate=0
[handlers]
keys=hand01,hand02,hand03
[handler_hand01]
class=StreamHandler
level=INFO
formatter=fmt01
args=(sys.stderr,)
[handler_hand02]
class=FileHandler
level=DEBUG
formatter=fmt02
args=("test.log","a")
[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=fmt02
args=("test.log","a",10*1024*1024,5)
[formatters]
keys=fmt01,fmt02
[formatter_fmt01]
format=%(asctime)s %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s
datefmt=%a %b %Y %H:%M:%S
[formatter_fmt02]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
4.3 配置日志程序的三种形式-03-创建配置信息字典并将其传递给dictConfig()函数
import logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
'format': '%(asctime)s [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s'
},
'standard': {
'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s'
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"default": {
"class": "logging.handlers.RotatingFileHandler", #日志切割,5M切割
"level": "INFO",
"formatter": "standard",
"filename": 'test3.log',
'mode': 'a',
"maxBytes": 1024*1024*5,
"backupCount": 5,
"encoding": "utf8"
},
},
"loggers": {
"test": {
"level": "WARNING",
"handlers": ["console"],
"propagate": "no"
}
},
"root": {
'handlers': ['default'],
'level': "INFO",
'propagate': False
}
}
logging.config.dictConfig(LOGGING)
Logger = logging.getLogger('test')
5、当logging遇上多进程
本文,我们详细介绍了python自带的logging模块的使用。然而,python中的logging貌似不支持多进程模块,多进程下的日志切割将会出现问题。针对这个问题,我们下期详解。同时介绍几种常见的替代解决方案。
参考:
【1】https://www.bilibili.com/video/BV1xB4y1s7RC/p=149&vd_source=12e876d9321bec4269ae1f826cf80ada
【2】https://zhuanlan.zhihu.com/p/476549020