Django如何使用loguru优雅地替换logging,并做到低侵入性
场景简介
python的logging功能十分的强大,但火热的loguru同样令人神往。一个是原配,一个是小三,家花还是野花香?作为成熟的程序猿,我当然是全都要,怎么做到不惊动logging的情况下,使用loguru神不知鬼不觉地接管Django的后宫日志大权,就是穿着品如的衣服,还有着爱莉的骚气【手动滑稽】。
ps:此操作需要一定的编程基础,大佬请忽略。
- 【低侵入性】不改动任何代码,还是使用logging的接口logging.info(),没有loguru包一样起飞
- 【可读性】添加不同类别的日志文件分类(debug、error、sql_debug),妈妈再也不用担心我不会看日志了。
loguru简介
简介个屁
代码原理
就是构建一个handler代理,所有的logging日志,这个handler(你可以理解为品如的衣服)下,其实是loguru在emit日志(有种NTR的感觉),这样通过一些简单的配置就能实现上层接口不变的情况下,使用不同的日志管理器,除了loguru、colorlog、nb_log你甚至可以自己做一个,替换进去就可以。
- settings.py
# 指定日志的目录所在,如果不存在则创建
LOG_ROOT = os.path.join(BASE_DIR, 'log')
if not os.path.exists(LOG_ROOT):
os.mkdir(LOG_ROOT)
# 日志配置(基本跟原生的TimedRotatingFileHandler一样)
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'standard': {
'format': '[%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s] '
'[%(levelname)s]- %(message)s'},
'simple': { # 简单格式
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'servers': {
'class': 'common.utils.log.InterceptTimedRotatingFileHandler', # 这个路径看你本地放在哪里(下面的log文件)
'filename': os.path.join(LOG_ROOT, 'srap.log'),
'when': "D",
'interval': 1,
'backupCount': 1,
'formatter': 'standard',
'encoding': 'utf-8',
},
'db': {
'class': 'common.utils.log.InterceptTimedRotatingFileHandler', # 这个路径看你本地放在哪里
'filename': os.path.join(LOG_ROOT, 'srap_db.log'),
'when': "D",
'interval': 1,
'backupCount': 1,
'formatter': 'standard',
'encoding': 'utf-8',
'logging_levels': ['debug'] # 😒注意这里,这是自定义类多了一个参数,因为我只想让db日志有debug文件,所以我只看sql,这个可以自己设置
}
},
'loggers': {
# Django全局绑定
'django': {
'handlers': ['servers'],
'propagate': True,
'level': "INFO"
},
'celery': {
'handlers': ['servers'],
'propagate': False,
'level': "INFO"
},
'django.db.backends': {
'handlers': ['db'],
'propagate': False,
'level': "DEBUG"
},
'django.request': {
'handlers': ['servers'],
'propagate': False,
'level': "DEBUG"
},
}
}
- log.py
import logging
import os.path
from loguru import logger
# 1.🎖️先声明一个类继承logging.Handler(制作一件品如的衣服)
class InterceptTimedRotatingFileHandler(logging.Handler):
"""
自定义反射时间回滚日志记录器
缺少命名空间
"""
def __init__(self, filename, when='d', interval=1, backupCount=15, encoding="utf-8", delay=False, utc=False,
atTime=None, logging_levels="all"):
super(InterceptTimedRotatingFileHandler, self).__init__()
filename = os.path.abspath(filename)
when = when.lower()
# 2.🎖️需要本地用不同的文件名做为不同日志的筛选器
self.logger_ = logger.bind(sime=filename)
self.filename = filename
key_map = {
'h': 'hour',
'w': 'week',
's': 'second',
'm': 'minute',
'd': 'day',
}
# 根据输入文件格式及时间回滚设立文件名称
rotation = "%d %s" % (interval, key_map[when])
retention = "%d %ss" % (backupCount, key_map[when])
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
if when == "s":
time_format = "{time:%Y-%m-%d_%H-%M-%S}"
elif when == "m":
time_format = "{time:%Y-%m-%d_%H-%M}"
elif when == "h":
time_format = "{time:%Y-%m-%d_%H}"
elif when == "d":
time_format = "{time:%Y-%m-%d}"
elif when == "w":
time_format = "{time:%Y-%m-%d}"
level_keys = ["info"]
# 3.🎖️构建一个筛选器
levels = {
"debug": lambda x: "DEBUG" == x['level'].name.upper() and x['extra'].get('sime') == filename,
"error": lambda x: "ERROR" == x['level'].name.upper() and x['extra'].get('sime') == filename,
"info": lambda x: "INFO" == x['level'].name.upper() and x['extra'].get('sime') == filename,
"warning": lambda x: "WARNING" == x['level'].name.upper() and x['extra'].get('sime') == filename}
# 4. 🎖️根据输出构建筛选器
if isinstance(logging_levels, str):
if logging_levels.lower() == "all":
level_keys = levels.keys()
elif logging_levels.lower() in levels:
level_keys = [logging_levels]
elif isinstance(logging_levels, (list, tuple)):
level_keys = logging_levels
for k, f in {_: levels[_] for _ in level_keys}.items():
# 5.🎖️为防止重复添加sink,而重复写入日志,需要判断是否已经装载了对应sink,防止其使用秘技:反复横跳。
filename_fmt = filename.replace(".log", "_%s_%s.log" % (time_format, k))
# noinspection PyUnresolvedReferences,PyProtectedMember
file_key = {_._name: han_id for han_id, _ in self.logger_._core.handlers.items()}
filename_fmt_key = "'{}'".format(filename_fmt)
if filename_fmt_key in file_key:
continue
# self.logger_.remove(file_key[filename_fmt_key])
self.logger_.add(
filename_fmt,
retention=retention,
encoding=encoding,
level=self.level,
rotation=rotation,
compression="tar.gz", # 日志归档自行压缩文件
delay=delay,
enqueue=True,
filter=f
)
def emit(self, record):
try:
level = self.logger_.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
# 6.🎖️把当前帧的栈深度回到发生异常的堆栈深度,不然就是当前帧发生异常而无法回溯
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
self.logger_.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
使用
import logging
logging.info("继续使用logging作为上层接口,不影响现有项目已有的日志记录方式")
附图一张。
-
db_debug.log
-
error.log
- info.log
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具