You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何解决Loggerator日志装饰器导入后filename与funcName取自装饰器文件而非业务文件的问题

解决装饰器日志中filename/funcName指向装饰器文件的问题

这个问题其实很常见——当你在装饰器类的__call__方法里调用日志输出时,logging模块默认会从当前执行的栈帧(也就是Loggerator.__call__所在的上下文)提取filenamefuncName这些属性,自然就会指向Loggerator.py而不是你的业务代码文件。下面给你两种实用的解决方案:

方案一:自定义Logger跳过装饰器栈帧(推荐,无需修改日志格式)

这种方法通过重写Logger的findCaller方法,让它自动跳过装饰器的调用栈帧,直接获取业务代码的上下文信息:

# Loggerator.py文件
import logging
from functools import wraps

# 自定义Logger,让它跳过装饰器的栈帧
class CustomLogger(logging.Logger):
    def findCaller(self, stack_info=False, stacklevel=1):
        # 因为装饰器的__call__是额外的一层栈,所以我们多跳1层
        return super().findCaller(stack_info, stacklevel + 1)

# 注册自定义Logger类,让后续getLogger都用这个类
logging.setLoggerClass(CustomLogger)

class Loggerator:
    def __init__(self, func):
        # 用functools.wraps保留原函数的元信息(可选,但推荐)
        wraps(func)(self)
        logging.basicConfig(
            filename='example3.log',
            format='%(asctime)s.%(msecs)03d | %(levelname)-8s | %(name)s | %(filename)s.%(funcName)s(%(lineno)d) | %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
            level=logging.DEBUG,
            encoding='utf-8'
        )
        self.func = func
        self.log = logging.getLogger(func.__name__)

    def __call__(self, *args, **kwargs):
        try:
            self.log.debug(f'call {self.func.__name__}')
            result = self.func(*args, **kwargs)
            self.log.debug(f'end of {self.func.__name__}')
            return result
        except Exception as err:
            self.log.exception(err)
            return err

这样修改后,你的test.py完全不用改动,日志里的filename会显示test.pyfuncName会显示multlineno也会指向调用mult的行号。

方案二:手动传递业务函数上下文(灵活,适合需要自定义字段的场景)

如果需要同时保留装饰器和业务函数的信息,或者不想修改全局Logger类,可以用inspect模块获取业务代码的栈帧信息,通过extra参数传给日志:

# Loggerator.py文件
import logging
import inspect
from functools import wraps

class Loggerator:
    def __init__(self, func):
        wraps(func)(self)
        # 修改日志格式,使用自定义的业务字段
        logging.basicConfig(
            filename='example3.log',
            format='%(asctime)s.%(msecs)03d | %(levelname)-8s | %(name)s | %(biz_filename)s.%(biz_funcName)s(%(biz_lineno)d) | %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
            level=logging.DEBUG,
            encoding='utf-8'
        )
        self.func = func
        self.log = logging.getLogger(func.__name__)

    def __call__(self, *args, **kwargs):
        # 获取调用业务函数的栈帧(跳过装饰器的__call__)
        frame = inspect.currentframe().f_back
        try:
            # 提取业务代码的上下文信息
            biz_context = {
                'biz_filename': frame.f_code.co_filename.split('/')[-1],  # 只保留文件名,不带路径
                'biz_funcName': frame.f_code.co_name,
                'biz_lineno': frame.f_lineno
            }
            self.log.debug(f'call {self.func.__name__}', extra=biz_context)
            result = self.func(*args, **kwargs)
            self.log.debug(f'end of {self.func.__name__}', extra=biz_context)
            return result
        except Exception as err:
            self.log.exception(err, extra=biz_context)
            return err
        finally:
            # 手动删除栈帧引用,避免内存泄漏
            del frame

这种方法需要修改日志格式,用自定义的biz_filenamebiz_funcName等字段,好处是可以灵活控制要输出的信息,甚至同时输出装饰器和业务代码的上下文。

注意事项

  • 方案一中的stacklevel +1是因为你的装饰器只有一层,如果是多层嵌套装饰器,需要根据实际层数调整增量。
  • 方案二中使用inspect时,一定要记得在finally里删除frame引用,否则可能会造成内存泄漏。

内容的提问来源于stack exchange,提问作者Кирилл Фролов

火山引擎 最新活动