本文是loguru的doc的阅读笔记,原文链接为:原文链接
loguru
是一个替代 Python logging 的第三方库:简单易用且功能强大。
Loguru 仅使用一个全局 logger
实例
- 在整个进程中,无需创建多个 logger 实例,而是使用一个预配置的单一 logger,这与Python标准的
logging
库的使用方式不同。 - 使用一个全局的 logger 可以简化配置,方便使用,您可以在任何地方导入 logger 以直接使用,同时确保日志的格式和处理方法在整个进程中保持一致。
- 尽管只有一个全局的 logger 对象,但
loguru
允许用户使用bind
来添加自定义的上下文属性,并提供了 filter 来区分不同的日志源,并进行不同的处理。
用 add()
代替了 handlers
、formatters
、filters
add()
方法允许进行以下设置:
- 输出目标(
sink
):可以是文件名、文件对象、标准输出、自定义流、远程服务器或数据库。 - 日志格式:可以自定义日志的显示格式。
- 日志级别:您可以全局设置,或为每个输出目标设置单独的级别。
- 过滤器:可以是一个函数或条件表达式,用于决定是否输出日志。
- 文件的压缩和大小限制:根据时间和文件大小设置自动覆盖轮转,并能自动压缩日志文件。
logger.add(sys.stderr, format="{time} {level} {message}", filter="my_module", level="INFO")
logger.add("file_{time}.log")
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
加强的文本格式化
- 可以直接在日志字符串中使用
{}
来插入变量或者表达式。 - 日志格式支持富文本和颜色编码。
from loguru import logger
score = 100.0
user = {'name': 'Ben', 'age': 18}
logger.info("{}, {s}, {name} and {age}", score, s=score, **user)
logger.info("If you're using Python {}, prefer {feature} of course!", 3.9, feature="f-strings")# Pretty logging with color
logger.add(sys.stdout, colorize=True, format="<green>{time}</green> <level>{message}</level>")
- 消息的文本和值紧密相连,便于阅读。
- 花括号内支持表达式,具有灵活性。
loguru
对表达式的求值采用延迟计算,若不满足记录条件,则不进行求值。
用 catch 记录未捕获的异常
- 在
loguru
中,catch
是一个装饰器,它可以应用于一个函数上,用以捕获该函数抛出的异常。 - 在
loguru
中,catch
可以作为上下文管理器,与with
一起用于捕获抛出的异常。
from loguru import logger
@logger.catch
def thread_function_with_exeption():raise ValueError('An Exeption!')def foo():with logger.catch():raise ValueError('An Exception!')
线程安全、多进程、异步日志记录
- 在
loguru
中,sink
是线程安全的。 - 在多进程环境中,如果多个进程尝试写入同一个日志文件,可能会导致数据损坏。为了避免这种情况,可以在使用
add
方法时加上enqueue=True
参数,这将使得日志消息通过消息队列进行管理。 - 在高性能应用中,使用
enqueue=True
可以让消息处理在后台线程中执行,从而减少对主应用性能的影响。 - 如果你的
sink
是一个协程函数,例如,你添加了一个协程来将日志保存到远程服务或数据库,记得在最后调用loguru.complete()
以确保所有的异步日志任务已完成。
import asyncio
from loguru import loggerasync def async_sink(message):await async_operation(message)logger.add(async_sink)async def main():logger.info('my message')await logger.complete()asyncio.run(main())
完整的异常描述
- 能够完整地记录异常信息,包括堆栈跟踪(stack trace)和触发异常时的变量值。
# 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 / bdef nested(c):try:func(5, c)except ZeroDivisionError:logger.exception("What?!")nested(0)> 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
结构化和序列化
- 将日志记录序列化为结构化的JSON,以便传递给其他模块进行解析。
logger.add(sys.stdout, serialize=True)
logger.info('Logger initialized')> {
> "text": "2024-04-28 15:13:22.256 | INFO | __main__:<module>:32 - Logger initialized\\n",
> "record": {
> "elapsed": {
> "repr": "0:00:00.244809",
> "seconds": 0.244809
> },
> "exception": null,
> "extra": {},
> "file": {
> "name": "my_test.py",
> "path": ".../my_test.py"
> },
> "function": "<module>",
> "level": {
> "icon": "ℹ️",
> "name": "INFO",
> "no": 20
> },
> "line": 32,
> "message": "Logger initialized",
> "module": "my_test",
> "name": "__main__",
> "process": {
> "id": 66324,
> "name": "MainProcess"
> },
> "thread": {
> "id": 7993007168,
> "name": "MainThread"
> },
> "time": {
> "repr": "2024-04-28 15:13:22.256298+08:00",
> "timestamp": 1714288402.256298
> }
> }
> }
logger 的上下文管理
- 可以为日志消息添加额外的上下文信息,这等同于为每一条相关的日志消息增加了一些 Record 属性,这些属性会与日志一起被记录。
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")
- 通过结合使用
bind()
和filter
,您可以更精确地控制日志输出。
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()
方法在写日志之前动态添加属性。
logger.add(sys.stderr, format="{extra[utc]} {message}")
logger = logger.patch(lambda record: record["extra"].update(utc=datetime.utcnow()))
- 使用
contextualize()
方法临时修改上下文
with logger.contextualize(task=task_id):...logger.info("End of task")``
# 定义一个函数来创建格式化字符串,需要包含特定的 extra 字段
def custom_format(record):task_value = record["extra"].get("task", "N/A") # 从extra获取task字段,如果没有则返回"N/A"return f"{record['time']} {record['level']} {record['message']} | task={task_value}"logger.add(sink_obj, format=custom_format, level="INFO")task_id = ...with logger.contextualize(task=task_id):logger.info("End of task")
logger.opt()
方法
logger.opt(lazy=True)
:只有 message 需要输出的时候才去对其求值,这样可以避免一些计算代价比较高的运算。logger.opt(exception=True)
:在日志消息中添加异常堆栈跟踪。logger.opt(depth=1)
:改变堆栈跟踪深度logger.opt(color=True)
:在日志消息中使用颜色。logger.opt(record=True)
:在日志消息记录线程 ID 等各种属性。logger.opt(raw=True)
:绕过日志的格式化器直接输出。logger.opt(capture=False)
:是否将关键字参数自动添加到日志记录的extra
。
def my_decorator(func):def wrapper(*args, **kwargs):logger.info(f"Calling function: {func.__name__}")logger.opt(depth=1).info(f"Calling function: {func.__name__}")result = func(*args, **kwargs)return resultreturn wrapper@my_decorator
def some_function():logger.info("Inside some_function")some_function()
2024-05-06 15:06:25.984 | INFO | __main__:wrapper:5 - Calling function: some_function
2024-05-06 15:06:25.984 | INFO | __main__:<module>:15 - Calling function: some_function
2024-05-06 15:06:25.985 | INFO | __main__:some_function:13 - Inside some_function
loguru 自定义日志级别
- 创建一个新的日志级别:
new_level = logger.level("SNAKY", no=38, color="<yellow>", icon="🐍")
"SNAKY"
: 新日志级别的名称。no=38
: 日志级别的数值。日志级别的数值很重要,因为它决定了日志消息的重要性。数值越低,级别越高。例如,标准的DEBUG
级别是 10,而CRITICAL
是 50。color="<yellow>"
: 在支持颜色的终端中,此级别的日志消息将以黄色显示。icon="🐍"
: 这个级别的日志消息将以蛇形图标(🐍)作为前缀。
- 使用自定义的日志级别:
logger.log("SNAKY", "Here we go!")
loguru
增加了两个非标准的级别:trace
和success
trace
级别在日志级别层次中处于最低端,低于DEBUG
级别,用于输出极其详细的执行信息的情况,比如复杂的问题排查(troubleshooting)和调试,可以使用它来记录函数的详细调用参数、返回值或是中间计算的状态。success
级别用于记录操作成功完成的情况,高于INFO
级别,但低于WARNING
级别,为成功事件提供了一个明确的记录点。例如,在一个长时间运行的数据处理任务成功完成后,或是在用户完成关键的账户设置步骤后。
日期的简化和优化
- 在传统的 Python logging 模块中,处理日期和时间往往需要手动设置多个参数,如
datefmt
、在格式字符串中使用%(asctime)s
和%(created)s
等,而且默认、间戳不带时区信息(naive datetimes)。 loguru
可以直接在日志格式字符串中指定时间的格式logger.add("file.log", format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}")
loguru
默认支持包含时区信息的日期和时间
通过 configure()
配置 loguru
- 通过脚本配置 loguru
import sys
from loguru import loggerconfig = {"handlers": [{"sink": sys.stdout, "format": "{time} - {message}"},{"sink": "file.log", "serialize": True},],"extra": {"user": "someone"}
}
logger.configure(**config)
- 通过
loguru-config
库加在配置文件- https://github.com/erezinman/loguru-config
- 禁用/启用 Python 库中的 loguru
- Library 应该先调用
configure()
,岁后disable()
loguru
- Library 应该先调用
# For libraries, should be your library's `__name__`
import mylibmy_lib_name = mylib.__name__logger.disable(my_lib_name)
logger.info("No matter added sinks, this message is not displayed")# In your application, enable the logger in the `library`
logger.enable(my_lib_name)
logger.info("This message however is propagated to the sinks")
通过环境变量修改默认配置
- 完整的环境变量列表:https://github.com/Delgan/loguru/blob/master/loguru/_defaults.py
# Linux / OSX
export LOGURU_FORMAT="{time} | <lvl>{message}</lvl>"# Windows
setx LOGURU_DEBUG_COLOR "<green>"
loguru
自带的 parse
loguru
提供了内置parse()
用来解析日志文件,获取信息
# time: 匹配任何字符,直到遇到 " - "
# level: 匹配一个或多个数字
# message: 匹配任何字符直到行末
pattern = r"(?P<time>.*) - (?P<level>[0-9]+) - (?P<message>.*)"
# 定义从字符串到对应数据类型的转换器
caster_dict = dict(time=dateutil.parser.parse, level=int)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
和内置 logging
完全兼容
- 可以直接
add
内置的handler
import logging
from loguru import loggerhandler = logging.handlers.SysLogHandler(address=('localhost', 514))
logger.add(handler)
- 将
loguru
消息传递到内置logging
import logging
from loguru import loggerclass PropagateHandler(logging.Handler):def emit(self, record: logging.LogRecord) -> None:logging.getLogger(record.name).handle(record)logger.add(PropagateHandler(), format="{message}")
- 拦截 内置
logging
消息到loguru
class InterceptHandler(logging.Handler):def emit(self, record: logging.LogRecord) -> None:level: str | int# 尝试获取与标准 logging 等级相对应的 Loguru 日志等级try:level = logger.level(record.levelname).nameexcept ValueError:# 如果找不到对应的 Loguru 等级,则使用原始的数字等级level = record.levelno# 探测调用日志的代码位置frame, depth = inspect.currentframe(), 0while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):frame = frame.f_backdepth += 1# 使用 Loguru 记录日志信息,保持调用栈的深度和异常信息logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
- 用
loguru
拦截logging
的消息需要的信息包括level
、frame
深度、消息记录。 level: str | int
是类型提示,字符串或者整数- 先试图将
level name
转为对应的level
,如果失败则使用int
型的level
数值 - 从当前的
frame
通过f_back
追溯到调用logging
的具体代码位置 - 用
opt()
函数设置loguru
- 用
basicConfig
设置全局转发 level
为 0 会转发所有level
的消息force
确保无论之前日志系统如何配置,都将应用我们的配置,使得InterceptHandler
生效
与 notifiers
库结合使用
notifiers
库需要事先单独安装
import notifiersparams = {"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 NotificationHandlerhandler = NotificationHandler("gmail", defaults=params)
logger.add(handler, level="ERROR")