在 Python 中用到日志记录,那就不可避免地会用到内置的 logging
标准库 。虽然 logging
库采用的是模块化设计,你可以设置不同的 handler
来进行组合,但是在配置上通常较为繁琐;而且如果不是特别处理,在一些多线程或多进程的场景下使用 logging
还会导致日志记录会出现错乱或是丢失的情况。
loguru
能够减少繁琐的配置过程还能实现和 logging
类似的功能,同时还能保证日志记录的线程进程安全,又能够和 logging
相兼容,并进一步追踪异常也能进行代码回溯。loguru
库的使用也分简单,我们直接可以通过导入它本身封装好的 logger
类就可以直接进行调用。
#!pip install loguru
from loguru import logger
logger
本身就是一个已经实例化好的对象,如果没有特殊的配置需求,那么自身就已经带有通用的配置参数;同时它的用法和 logging
库输出日志时的用法一致。
In [1]: from loguru import logger
...:
...: logger.debug("debug message" )
...: logger.info("info level message")
...: logger.warning("warning level message")
...: logger.critical("critical level message")
2020-10-07 14:23:09.637 | DEBUG | __main__:<module>:3 - debug message
2020-10-07 14:23:09.637 | INFO | __main__:<module>:4 - info level message
2020-10-07 14:23:09.638 | WARNING | __main__:<module>:5 - warning level message
2020-10-07 14:23:09.638 | CRITICAL | __main__:<module>:6 - critical level message
在不指定任何参数时,logger
默认采用 sys.stderr
标准错误输出将日志输出到控制台(console
)中;但在 linux 服务器上我们有时不仅让其输出,还要以文件的形式进行留存,那么只需要在第一个参数中传入一个你想要留存文件的路径字符串即可。就像这样:
from loguru import logger
import os
logger.add(os.path.expanduser("~/Desktop/testlog.log"))
logger.info("hello, world!")
通常来说如果程序或服务的量级较大,那么就可以通过集成的日志平台或数据库来对日志信息进行存储和留存,后续有需要的话也方便进行日志分析。
但对我们个人或者一些中小型项目来说,通常只需要以文件的形式留存输出的日志即可。
尽管我们需要将日志写入到相应的文件中,如果是少量的日志那还好,但是如果是日志输出或记录时间较长的情况,那么单个日志文件就十分之大,倘若仍然是将日志都写入到一个文件中,那么当日志中的内容增长到一定数量时我们想要读取并查找相应的部分时就十分困难。这时候我们就需要对日志文件进行留存、压缩,甚至在必要时及时进行清理。
基于以上,我们可以通过对 rotation
、compression
和 retention
三个参数进行设定来满足我们的需要:
rotation
参数能够帮助我们将日志记录以大小、时间等方式进行分割或划分:
import os
from loguru import logger
LOG_DIR = os.path.expanduser("~/Desktop/logs")
LOG_FILE = os.path.join(LOG_DIR, "file_{time}.log")
if os.path.exits(LOG_DIR):
os.mkdir(LOG_DIR)
logger.add(LOG_FILE, rotation = "200KB")
for n in range(10000):
logger.info(f"test - {n}")
随着分割文件的数量越来越多之后,我们也可以进行压缩对日志进行留存,这里就要使用到 compression
参数,该参数只要你传入通用的压缩文件扩展名即可,如 zip
、tar
、gz
等。
import os
from loguru import logger
LOG_DIR = os.path.expanduser("~/Desktop/logs")
LOG_FILE = os.path.join(LOG_DIR, "file_{time}.log")
if os.path.exits(LOG_DIR):
os.mkdir(LOG_DIR)
logger.add(LOG_FILE, rotation = "200KB", compression="zip")
for n in range(10000):
logger.info(f"test - {n}")
当然了,如果你不想对日志进行留存,或者只想保留一段时间内的日志并对超期的日志进行删除,那么直接使用 retention
参数就好了。
这里我们可以将之前的结果随意复制 N 多份在 logs
文件夹中,然后再执行一次加上 retension
参数后代码:
from loguru import logger
LOG_DIR = os.path.expanduser("~/Desktop/logs")
LOG_FILE = os.path.join(LOG_DIR, "file_{time}.log")
if not os.path.exists(LOG_DIR):
os.mkdir(LOG_DIR)
logger.add(LOG_FILE, rotation="200KB",retention=1)
for n in range(10000):
logger.info(f"test - {n}")
如果在实际中你不太喜欢以文件的形式保留日志,那么你也可以通过 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!")
当异常和错误不可避免时,最好的方式就是让我们知道程序到底是哪里出了错,或者是因为什么导致错误,这样才能更好地让开发人员及时应对并解决。
当遇到错误是,如果在打印出log的时候没有配置 Traceback 的输出,很有可能无法追踪错误。loguru提供了装饰器就可以直接进行 Traceback 的记录。
from loguru import logger
@logger.catch()
def test():
return 1/0
test()
loguru集成了一个名为better_exceptions 的库,不仅能够将异常和错误记录,并且还能对异常进行追溯,这里是来自一个官网的例子
import os
import sys
from loguru import logger
logger.add(os.path.expanduser("~/Desktop/exception_log.log"), backtrace=True, diagnose=True)
def func(a, b):
return a / b
def nested(c):
try:
func(5, c)
except ZeroDivisionError:
logger.exception("What?!")
if __name__ == "__main__":
nested(0)
最后在日志文件中我们可以得到以下内容:
File "/Users/Bobot/PycharmProjects/docs-python/src/loguru/log_test.py", line 20, in <module>
nested(0)
└ <function nested at 0x7fb9300c1170>
> File "/Users/Bobot/PycharmProjects/docs-python/src/loguru/log_test.py", line 14, in nested
func(5, c)
│ └ 0
└ <function func at 0x7fb93010add0>
File "/Users/Bobot/PycharmProjects/docs-python/src/loguru/log_test.py", line 10, in func
return a / b
│ └ 0
└ 5
ZeroDivisionError: division by zero
尽管说 loguru
算是重新「造轮子」,但是它也能和 logging
库很好地兼容。到现在我们才谈论到 add()
方法的第一个参数 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
即可。
import logging.handlers
import os
import sys
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
logging.basicConfig(handlers=[InterceptHandler()], level=0)
def func(a, b):
return a / b
def nested(c):
try:
func(5, c)
except ZeroDivisionError:
logging.exception("What?!")
if __name__ == "__main__":
nested(0)
后结果同之前的异常追溯一致。而我们只需要在配置后直接调用 logging
的相关方法即可,减少了迁移和重写的成本。