Python 日志管理模块loguru
Python中的日志管理模块可以使用自带的logging,也可使用第三方Loguru模块,使用logging需要配置Handler、Formatter 进行一些处理,配置比较繁琐,
而使用Loguru则较为简单。
安装
pip install loguru
基本使用
loguru库的使用可以说是十分简单,我们直接可以通过导入它本身封装好的logger 类就可以直接进行调用。
logger本身就是一个已经实例化好的对象,如果没有特殊的配置需求,那么自身就已经带有通用的配置参数;同时它的用法和logging库输出日志时的用法一致。
from loguru import logger logger.debug('this is a debug message') logger.info('this is info message') logger.warning('this is warning message') logger.error('this is error message') logger.info('this is info message') logger.success('this is success message!') logger.critical('this is critical message!')
执行结果 :
从loguru库引入logger后,直接调用其 info,debug,error 方法即可。可以看到其默认的输出格式是上面的内容,有时间、级别、模块名、行号以及日志信息,不需要手动创建 logger,直接使用即可。上面的日志信息是直接输出在控制台的,如果想要输出到其他的位置,比如存为文件,我们只需要使用一行代码声明即可。例如将结果同时输出到一个 runtime.log 文件里面,可以这么写:
from loguru import logger logger.add('runtime.log') logger.debug('this is a debug message') logger.info('this is info message') logger.warning('this is warning message') logger.error('this is error message') logger.info('this is info message') logger.success('this is success message!') logger.critical('this is critical message!')
执行结果:
运行之后会发现目录下 my_log.log 出现了刚刚控制台输出的 DEBUG 信息。上面只是基础用法,更详细的在下面。
详细使用
loguru 对输出到文件的配置有非常强大的支持,比如支持输出到多个文件,分级别分别输出,过大创建新文件,过久自动删除等等。
(一)add 方法定义
def add( self, sink, *, level=_defaults.LOGURU_LEVEL, format=_defaults.LOGURU_FORMAT, filter=_defaults.LOGURU_FILTER, colorize=_defaults.LOGURU_COLORIZE, serialize=_defaults.LOGURU_SERIALIZE, backtrace=_defaults.LOGURU_BACKTRACE, diagnose=_defaults.LOGURU_DIAGNOSE, enqueue=_defaults.LOGURU_ENQUEUE, catch=_defaults.LOGURU_CATCH, **kwargs ): pass
skin参数可以传入多种不同的数据结构,如下:
- sink 可以传入一个 file 对象,例如 sys.stderr 或者 open('file.log', 'w') 都可以。
- sink 可以直接传入一个 str 字符串或者 pathlib.Path 对象,其实就是代表文件路径的,如果识别到是这种类型,它会自动创建对应路径的日志文件并将日志输出进去。
- sink 可以是一个方法,可以自行定义输出实现。
- sink 可以是一个 logging 模块的 Handler,比如 FileHandler、StreamHandler 等等,或者上文中我们提到的 CMRESHandler 照样也是可以的,这样就可以实现自定义 Handler 的配置。
- sink 还可以是一个自定义的类,具体的实现规范可以参见官方文档。https://loguru.readthedocs.io/en/stable/api/logger.html#sink
(二) 基本参数
其他参数例如 format、filter、level 等等。其实它们的概念和格式和 logging 模块都是基本一样的了,例如这里使用 format、filter、level 来规定输出的格式:
logger.add('runtime.log', format="{time} {level} {message}", filter="my_module", level="INFO",encoding="utf-8"
)
(三)删除 sink
添加 sink 之后可以对其进行删除,相当于重新刷新并写入新的内容。删除的时候根据刚刚 add 方法返回的 id 进行删除即可,看下面的例子:
from loguru import logger trace = logger.add('my_log.log') logger.debug('this is a debug message') logger.remove(trace) logger.debug('this is another debug message')
看这里,我们首先 add了一个sink,然后获取它的返回值,赋值为tarce。随后输出了一条日志,然后将trace变量传给remove方法,再次输出一条日志,看看结果是怎样的。
控制台输出如下:
日志文件 my_log.log 内容如下:
(四)rotation 配置
用了 loguru 我们还可以非常方便地使用rotation配置,比如我们想一天输出一个日志文件,或者文件太大了自动分隔日志文件,我们可以直接使用add方法的rotation参数进行配置。
logger.add('runtime_{time}.log', rotation="500 MB") # log文件超过500M时会新建一个log文件
通过这样的配置我们就可以实现每 500MB 存储一个文件,每个 log 文件过大就会新创建一个 log 文件。我们在配置 log 名字时加上了一个 time 占位符,这样在生成时可以自动将时间替换进去,生成一个文件名包含时间的 log 文件。
使用 rotation 参数实现定时创建 log 文件
logger.add('runtime_{time}.log', rotation='00:00') # 每天0点新建一个log文件
另外我们也可以配置 log 文件的循环时间,比如每隔一周创建一个 log 文件,写法如下:
logger.add('runtime_{time}.log', rotation='1 week') #每隔一周创建一个 log 文件
(五) retention 配置
很多情况下,一些非常久远的 log 对我们来说并没有什么用处了,它白白占据了一些存储空间,不清除掉就会非常浪费。retention 这个参数可以配置日志的最长保留时间。
retention 这个参数可以配置日志的最长保留时间。
logger.add('runtime.log', retention='10 days') # 保留最新10天的log
(六)compression 配置
loguru 还可以配置文件的压缩格式,比如使用 zip 文件格式保存,示例如下:
logger.add('runtime.log', compression='zip') # 使用zip文件格式保存
(七)serialize 序列化
如果在实际中你不太喜欢以文件的形式保留日志,那么你也可以通过serialize参数将其转化成序列化的json格式,最后将导入类似于MongoDB、ElasticSearch 这类数NoSQL 数据库中用作后续的日志分析。
from loguru import logger import os logger.add(os.path.expanduser("~/Desktop/testlog.log"), serialize=True) logger.info("hello, world!")
最后保存的日志都是序列化后的单条记录:
{ "text": "2022-4-19 22:59:36.902 | INFO | __main__:<module>:6 - hello, world\n", "record": { "elapsed": { "repr": "0:00:00.005412", "seconds": 0.005412 }, "exception": null, "extra": {}, "file": { "name": "log_test.py", "path": "/Users/Bobot/PycharmProjects/docs-python/src/loguru/log_test.py" }, "function": "<module>", "level": { "icon": "\u2139\ufe0f", "name": "INFO", "no": 20 }, "line": 6, "message": "hello, world", "module": "log_test", "name": "__main__", "process": { "id": 12662, "name": "MainProcess" }, "thread": { "id": 4578131392, "name": "MainThread" }, "time": { "repr": "2022-4-19 22:59:36.902358+08:00", "timestamp": 1602066216.902358 } } }
(八)字符串格式化
loguru 在输出 log 的时候还提供了非常友好的字符串格式化功能,像这样:
logger.info('If you are using Python {}, prefer {feature} of course!', 3.6, feature='f-strings')
(九)Traceback 记录
在很多情况下,如果遇到运行错误,而我们在打印输出 log 的时候万一不小心没有配置好 Traceback 的输出,很有可能我们就没法追踪错误所在了。
但用了 loguru 之后,我们用它提供的装饰器就可以直接进行 Traceback 的记录,类似这样的配置即可:
@logger.catch def my_function(x, y, z): # An error? It's caught anyway! return 1 / (x + y + z)
我们做个测试,我们在调用时三个参数都传入 0,直接引发除以 0 的错误,看看会出现什么情况:
my_function(0, 0, 0)
运行完毕之后,可以发现 log 里面就出现了 Traceback 信息,而且给我们输出了当时的变量值,真的是不能再赞了!结果如下:
因此,用 loguru 可以非常方便地实现日志追踪,debug 效率可能要高上十倍了?
(十)与 Logging 完全兼容(Entirely Compatible)
尽管说loguru算是重新「造轮子」,但是它也能和logging库很好地兼容。到现在我们才谈论到dd()方法的第一个参数sink。
这个参数的英文单词动词有「下沉、浸没」等意,对于外国人来说在理解上可能没什么难的,可对我们国人来说,这可之前logging库中的handler概念还不好理解。好在前面我有说过,loguru和logging库的使用上存在相似之处,因此在后续的使用中其实我们就可以将其理解为 handler,只不过它的范围更广一些,可以除了 handler之外的字符串、可调用方法、协程对象等。
loguru 官方文档对这一参数的解释是:
object in charge of receiving formatted logging messages and propagating them to an appropriate endpoint.
翻译过来就是「一个用于接收格式化日志信息并将其传输合适端点的对象」,进一步形象理解就像是一个「分流器」.
import logging.handlers import os import sys from loguru import logger LOG_FILE = os.path.expanduser("~/Desktop/testlog.log") file_handler = logging.handlers.RotatingFileHandler(LOG_FILE, encoding="utf-8") logger.add(file_handler) logger.debug("hello, world")
当然目前只是想在之前基于logging写好的模块中集成loguru,只要重新编写一个继承自logging.handler 类并实现了emit()方法的Handler即可。
比如flask项目中,直接调用setup_loguru()
就可以了
def setup_loguru(app,log_level='WARNING'): logger.add( 'logs/{time:%Y-%m-%d}.log', level='DEBUG', format='{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}', backtrace=False, rotation='00:00', retention='20 days', encoding='utf-8' ) app.logger.addHandler(InterceptHandler()) logging.basicConfig(handlers=[InterceptHandler()], level=log_level)
(十一)Loguru模块下pytest和HTMLTestRunner生成的html没有显示日志内容
最近将原有logging日志系统替换成了loguru,loguru的好处不用多说,简单好用。配置起来也比lgging方便多了。封装代码如下
import time, os from loguru import logger LOG_DIR = os.path.abspath(os.path.dirname(__file__)) # 日志保存路径 class Log: """输出日志到文件和控制台""" def __init__(self): # 文件的命名 log_name = f"test_{time.strftime('%Y-%m-%d', time.localtime()).replace('-','_')}.log" log_path = os.path.join(LOG_DIR, log_name) # 判断日志文件夹是否存在,不存则创建 if not os.path.exists(LOG_DIR): os.mkdir(LOG_DIR) # 日志输出格式 formatter = "{time:YYYY-MM-DD HH:mm:ss} | {level}: {message}" # 日志写入文件 logger.add(log_path, # 写入目录指定文件 format=formatter, encoding='utf-8', retention='10 days', # 设置历史保留时长 backtrace=True, # 回溯 diagnose=True, # 诊断 enqueue=True, # 异步写入 # rotation="5kb", # 切割,设置文件大小,rotation="12:00",rotation="1 week" # filter="my_module" # 过滤模块 # compression="zip" # 文件压缩 ) def debug(self, msg): logger.debug(msg) def info(self, msg): logger.info(msg) def warning(self, msg): logger.warning(msg) def error(self, msg): logger.error(msg) log = Log()
将上述封装代码引入,生成的html仍没有日志。点击通过无法查看日志。
原来使用HTMLTestRunner生成html测试报告时,报告中只有console控制台上输出,logging的输出无法保存,如果要在报告中加入每一个测试用例执行的日志信息,则需要改HTMLTestRunner源码。
修改_TestResult类,同时别忘了在文件最上面import logging。
import logging
class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1, retry=0,save_last_try=False): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.skip_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error;3:skip), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] self.retry = retry self.trys = 0 self.status = 0 self.save_last_try = save_last_try self.outputBuffer = StringIO.StringIO() self.logger = logging.getLogger() # 新增这一行
startTest函数中初始化logging.Handler
def startTest(self, test): # test.imgs = [] test.imgs = getattr(test, "imgs", []) # TestResult.startTest(self, test) self.outputBuffer.seek(0) self.outputBuffer.truncate() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector # 新增如下代码 self.log_cap = StringIO.StringIO() self.ch = logging.StreamHandler(self.log_cap) self.ch.setLevel(logging.DEBUG) formatter = logging.Formatter('[%(levelname)s][%(asctime)s] [%(filename)s]->[%(funcName)s] line:%(lineno)d ---> %(message)s') self.ch.setFormatter(formatter) self.logger.addHandler(self.ch)
complete_output函数的返回值中加入logging存在内存中的输出,用换行符隔开
def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None # return self.outputBuffer.getvalue() return self.outputBuffer.getvalue()+'\n'+self.log_cap.getvalue() # 新增这一行
每个用例执行完后,清除handler,修改stopTest函数
def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. if self.retry and self.retry>=1: if self.status == 1: self.trys += 1 if self.trys <= self.retry: if self.save_last_try: t = self.result.pop(-1) if t[0]==1: self.failure_count -=1 else: self.error_count -= 1 test=copy.copy(test) sys.stderr.write("Retesting... ") sys.stderr.write(str(test)) sys.stderr.write('..%d \n' % self.trys) doc = getattr(test,'_testMethodDoc',u"") or u'' if doc.find('_retry')!=-1: doc = doc[:doc.find('_retry')] desc ="%s_retry:%d" %(doc, self.trys) if not PY3K: if isinstance(desc, str): desc = desc.decode("utf-8") test._testMethodDoc = desc test(self) else: self.status = 0 self.trys = 0 #self.complete_output() a = self.complete_output() # 清除log的handle,新增如下代码 self.logger.removeHandler(self.ch) return a
可能各自用的HTMLTestRunner版本内容不一样,均只需按照上述修改即可。
按照上述修改,再次运行,生成的html报告还是没有日志内容,会不会是loguru和logging不兼容?
import time, os, logging from loguru import logger from settings import LOG_DIR # 日志保存路径 # 新增如下三行代码 class PropogateHandler(logging.Handler): def emit(self, record): logging.getLogger(record.name).handle(record) class Log: """输出日志到文件和控制台""" def __init__(self): # 文件的命名 log_name = f"test_{time.strftime('%Y-%m-%d', time.localtime()).replace('-','_')}.log" log_path = os.path.join(LOG_DIR, log_name) # 判断日志文件夹是否存在,不存则创建 if not os.path.exists(LOG_DIR): os.mkdir(LOG_DIR) # 日志输出格式 formatter = "{time:YYYY-MM-DD HH:mm:ss} | {level}: {message}" # 日志写入文件 logger.add(log_path, # 写入目录指定文件 format=formatter, encoding='utf-8', retention='10 days', # 设置历史保留时长 backtrace=True, # 回溯 diagnose=True, # 诊断 enqueue=True, # 异步写入 # rotation="5kb", # 切割,设置文件大小,rotation="12:00",rotation="1 week" # filter="my_module" # 过滤模块 # compression="zip" # 文件压缩 ) # 新增代码 logger.add(PropogateHandler(), format=formatter) def debug(self, msg): logger.debug(msg) def info(self, msg): logger.info(msg) def warning(self, msg): logger.warning(msg) def error(self, msg): logger.error(msg) log = Log()
loguru二次封装,结合装饰器获取日志:
from loguru import logger import datetime import os,sys from functools import wraps class MyLoggers(object): def __init__(self,log_filename=None,is_stdout=True,need_write_log=True): self._logger = logger # 判断是否添加控制台输出的格式, sys.stdout为输出到屏幕; if is_stdout: # 清空所有设置 self._logger.remove() self._logger.add( sink=sys.stdout, format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | " # 颜色>时间 "{process.name} | " # 进程名 "{thread.name} | " # 线程名 "<cyan>{module}</cyan>.<cyan>{function}</cyan>" # 模块名.方法名 ":<cyan>{line}</cyan> | " # 行号 "<level>{level}</level>: " # 等级 "<level>{message}</level>", # 日志内容 ) # 判断是否需要写入logger日志文件 if need_write_log: # self._logger.remove(handler_id=None) #只写入文件中,不输入控制台 self._logger.add( # 水槽,分流器,可以用来输入路径 sink= self.get_log_path(log_filename=log_filename), # 日志创建周期 rotation='00:00', # 保存 retention='1 days', # 文件的压缩格式 compression='zip', # 编码格式 encoding="utf-8", # 具有使日志记录调用非阻塞的优点(适用于多线程) enqueue=True, # 日志级别 level='INFO', # 时间|进程名|线程名|模块|方法|行号|日志等级|日志信息 format="{time:YYYY-MM-DD HH:mm:ss} | {process.name} | {thread.name} | {module}.{function}:{line} | {level}:{message}" ) def get_log_path(self,log_filename=None): # 项目日志目录 project_log_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'log')) if not os.path.exists(project_log_dir): os.makedirs(project_log_dir) # 日志文件名 if log_filename is not None: project_log_filename = '{}.log'.format(log_filename) else: project_log_filename = 'runtime_{}.log'.format(datetime.date.today()) # 日志文件路径 project_log_path = os.path.join(project_log_dir, project_log_filename) return project_log_path def info(self,mesg): self._logger.info(mesg) def debug(self,mesg): self._logger.debug(mesg) def warning(self,mesg): self._logger.warning(mesg) def error(self,mesg): self._logger.error(mesg) def get_logger(self): return self._logger my_loger = MyLoggers(log_filename='mylog', need_write_log=True, is_stdout=True).get_logger() # 装饰器用来扑获日志(不带参数) def wrapper_log(func): @wraps(func) def inner(*args,**kwargs): my_loger.info(f'{func.__name__}') try: func(*args,**kwargs) except Exception as error_mesg: print(error_mesg) my_loger.exception(error_mesg) return inner() # 装饰器用来扑获日志(带参数) def loger(logger_object): def wrapper(func): def inner(*args,**kwargs): logger_object.info(f'{func.__name__}') try: func(*args,**kwargs) except Exception as error_mesg: logger_object.exception(error_mesg) return inner() return wrapper @wrapper_log def test_func(a,b): print(a/b) @loger(my_loger) def test_demo(a,b): return a/b test_func(1/0) test_demo(1/0)
原文来源于:https://blog.51cto.com/u_15127517/4403171
另外loguru还有很多很多强大的功能,这里就不再一一展开讲解了,更多的内容大家可以看看 loguru的官方文档详细了解一下:
https://loguru.readthedocs.io/en/stable/index.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!