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

 

posted @ 2022-04-19 23:13  浩浩学习  阅读(2323)  评论(0编辑  收藏  举报