Python日志库:Loguru

Loguru是一个旨在为Python带来愉快的日志记录的库。使用Loguru,没有理由不从一开始就使用日志记录,这就像从Loguru导入日志一样简单from loguru import logger。此外,这个库通过添加一系列有用的功能来解决使用标准日志记录库的痛苦。在应用程序中使用日志应该是自动的,Loguru试图使其既令人愉快又强大。

安装

1
pip install loguru

特性

开箱即用

Loguru的主要概念是有且只需要一个Logger。为了方便起见,它预先配置从输出到stderr开始(但是这是完全可配置的)。

1
2
3
from loguru import logger

logger.debug("That's it, beautiful and simple logging!")

Logger只是一个将日志消息分发给已配置的处理程序的接口。

无处理程序、无格式化、无过滤器:一个函数来规定所有

如何添加处理程序? 如何设置日志格式? 如何过滤消息? 如何设置级别?答案是:add()函数。

1
logger.add(sys.stderr, format="{time} {level} {message}", filter="my_module", level="INFO")

此函数用来注册使用record dict来管理log messages上下文的sinks。sink可以有多种形式:一个简单的函数、一个字符串路径、一个类似文件的对象、一个协程函数或一个内置的Handler。

注意,还可以通过使用在添加处理程序时返回的标识符来remove()以前添加的处理程序。如果希望取代默认的stderr处理程序,这尤其有用:只需调用logger.remove()来重新开始。

更容易的旋转/保留/压缩日志文件

如果将记录的消息发送到文件,只需使用字符串路径作为接收器。为了方便起见,它还可以自动计时:

1
logger.add("file_{time}.log")

如果需要旋转日志文件,或者删除较旧的日志文件,或者在关闭时压缩日志文件,也是很容易配置的。

1
2
3
4
5
6
7
logger.add("file_1.log", rotation="500 MB")    # Automatically rotate too big file
logger.add("file_2.log", rotation="12:00") # New file is created each day at noon
logger.add("file_3.log", rotation="1 week") # Once the file is too old, it's rotated

logger.add("file_X.log", retention="10 days") # Cleanup after some time

logger.add("file_Y.log", compression="zip") # Save some loved space

大括号样式的现代字符串格式设置

Loguru喜欢更优雅和强大的{}格式化而不是%,日志记录函数实际上等效于str.format ()。

1
logger.info("If you're using Python {}, prefer {feature} of course!", 3.6, feature="f-strings")

在线程或主线程中捕获异常

您是否曾经遇到过程序意外崩溃,而在日志文件中没有看到任何信息?您是否注意到线程中发生的异常没有被记录?可以使用catch ()装饰器/上下文管理器解决这个问题,该管理器确保任何错误都被正确地传递到logger。

1
2
3
4
@logger.catch
def my_function(x, y, z):
# An error? It's caught anyway!
return 1 / (x + y + z)

优美的彩色日志

如果终端兼容,Loguru会自动为日志添加颜色。可以在接收器格式中用markup tags来定义喜欢的样式。

1
logger.add(sys.stdout, colorize=True, format="<green>{time}</green> <level>{message}</level>")

异步、线程安全、多进程安全

默认情况下,添加到logger的所有接收器都是线程安全的。它们不是多进程安全的,但是可以对消息进行enqueue以确保日志的完整性。如果需要异步日志记录,也可以使用同样的参数。

1
logger.add("somefile.log", enqueue=True)

也支持作为接收器的协同函数,并且应该使用complete()进行等待。

完全描述例外

记录代码中发生的异常对于跟踪bug非常重要,但是如果不知道为什么会失败,那么记录异常就完全没有用处。Loguru允许显示整个堆栈跟踪,包括变量的值,从而帮助识别问题(感谢better_exception!)。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Caution, "diagnose=True" is the default and may leak sensitive data in prod
logger.add("out.log", backtrace=True, diagnose=True)

def func(a, b):
return a / b

def nested(c):
try:
func(5, c)
except ZeroDivisionError:
logger.exception("What?!")

nested(0)

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2018-07-17 01:38:43.975 | ERROR    | __main__:nested:10 - What?!
Traceback (most recent call last):

File "test.py", line 12, in <module>
nested(0)
└ <function nested at 0x7f5c755322f0>

> File "test.py", line 8, in nested
func(5, c)
│ └ 0
└ <function func at 0x7f5c79fc2e18>

File "test.py", line 4, in func
return a / b
│ └ 0
└ 5

ZeroDivisionError: division by zero

注意,由于帧数据不可用,这个特性不能在默认的Python REPL上工作。参考:Security considerations when using Loguru

根据需要结构化日志

是否希望将日志序列化以便于解析或传递它们?使用serialize序列化参数,每个日志消息在发送到配置的接收器之前都将转换为JSON字符串。

1
logger.add(custom_sink_function, serialize=True)

通过使用bind(),可以通过修改额外的record属性添加消息上下文。

1
2
3
4
5
logger.add("file.log", format="{extra[ip]} {extra[user]} {message}")
context_logger = logger.bind(ip="192.168.0.1", user="someone")
context_logger.info("Contextualize your logger easily")
context_logger.bind(user="someone_else").info("Inline binding of extra attribute")
context_logger.info("Use kwargs to add context during formatting: {user}", user="anybody")

可以使用contextualize()临时修改上下文局部状态:

1
2
3
with logger.contextualize(task=task_id):
do_something()
logger.info("End of task")

通过组合bind()和filter,可以对日志进行更细粒度的控制:

1
2
3
logger.add("special.log", filter=lambda record: "special" in record["extra"])
logger.debug("This message is not logged to the file")
logger.bind(special=True).info("This message, though, is logged to the file!")

最后,patch()方法允许将动态值附加到每条新消息记录:

1
2
logger.add(sys.stderr, format="{extra[utc]} {message}")
logger = logger.patch(lambda record: record["extra"].update(utc=datetime.utcnow()))

延迟求值代价函数

有时希望在生产环境中记录详细信息而不会影响性能,可以使用opt()方法来实现这一点。

1
2
3
4
5
6
7
8
9
logger.opt(lazy=True).debug("If sink level <= DEBUG: {x}", x=lambda: expensive_function(2**64))

# By the way, "opt()" serves many usages
logger.opt(exception=True).info("Error stacktrace added to the log message (tuple accepted too)")
logger.opt(colors=True).info("Per message <blue>colors</blue>")
logger.opt(record=True).info("Display values from the record (eg. {record[thread]})")
logger.opt(raw=True).info("Bypass sink formatting\n")
logger.opt(depth=1).info("Use parent stack context (useful within wrapped functions)")
logger.opt(capture=False).info("Keyword arguments not added to {dest} dict", dest="extra")

自定义级别

Loguru提供了所有添加trace()和success()的标准日志记录级别。是否需要更多?然后,使用level()函数创建它。

1
2
3
new_level = logger.level("SNAKY", no=38, color="<yellow>", icon="🐍")

logger.log("SNAKY", "Here we go!")

更好的处理日期时间

标准日志记录中充斥着诸如datefmt或msecs、%(asctime)s和%(created)s之类的参数,没有时区信息的朴素日期时间,没有直观的格式设置等等。Loguru解决了这个问题:

1
logger.add("file.log", format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}")

适用于脚本和库

在脚本中使用日志记录器很容易,并且可以在开始时configure()。要在库中使用Loguru,请记住永远不要调用add(),而是使用disable(),这样日志记录函数就变为no-op。如果开发人员希望查看库的日志,他们可以再次enable()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# For scripts
config = {
"handlers": [
{"sink": sys.stdout, "format": "{time} - {message}"},
{"sink": "file.log", "serialize": True},
],
"extra": {"user": "someone"}
}
logger.configure(**config)

# For libraries, should be your library's `__name__`
logger.disable("my_library")
logger.info("No matter added sinks, this message is not displayed")

# In your application, enable the logger in the library
logger.enable("my_library")
logger.info("This message however is propagated to the sinks")

为了更加方便,还可以使用loguru-config库直接从配置文件设置日志记录器。

与标准日志记录完全兼容

是否希望使用内置的日志Handler作为Loguru接收器?

1
2
handler = logging.handlers.SysLogHandler(address=('localhost', 514))
logger.add(handler)

是否需要将Loguru消息传播到标准日志?

1
2
3
4
5
class PropagateHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
logging.getLogger(record.name).handle(record)

logger.add(PropagateHandler(), format="{message}")

是否想要拦截到Loguru接收器的标准日志消息?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class InterceptHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
# Get corresponding Loguru level if it exists.
level: str | int
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno

# Find caller from where originated the logged message.
frame, depth = inspect.currentframe(), 0
while frame and (depth == 0 or 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, force=True)

通过环境变量的个性化默认值

是否不喜欢默认的日志记录器格式?喜欢另一种DEBUG颜色?没问题:

1
2
3
4
5
# Linux / OSX
export LOGURU_FORMAT="{time} | <lvl>{message}</lvl>"

# Windows
setx LOGURU_DEBUG_COLOR "<green>"

方便的解析器

从生成的日志中提取特定信息通常很有用,这就是Loguru提供parse()方法帮助处理日志和正则表达式的原因。

1
2
3
4
5
6
pattern = r"(?P<time>.*) - (?P<level>[0-9]+) - (?P<message>.*)"  # Regex with named groups
caster_dict = dict(time=dateutil.parser.parse, level=int) # Transform matching groups

for groups in logger.parse("file.log", pattern, cast=caster_dict):
print("Parsed:", groups)
# {"level": 30, "message": "Log example", "time": datetime(2018, 12, 09, 11, 23, 55)}

详尽的通知

Loguru可以很容易地与一流的notifiers库(必须单独安装)组合在一起,以便在程序意外失败时接收电子邮件或发送许多其他类型的通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import notifiers

params = {
"username": "you@gmail.com",
"password": "abc123",
"to": "dest@gmail.com"
}

# Send a single notification
notifier = notifiers.get_notifier("gmail")
notifier.notify(message="The application is running!", **params)

# Be alerted on each error message
from notifiers.logging import NotificationHandler

handler = NotificationHandler("gmail", defaults=params)
logger.add(handler, level="ERROR")