如何解决Loggerator日志装饰器导入后filename与funcName取自装饰器文件而非业务文件的问题
解决装饰器日志中filename/funcName指向装饰器文件的问题
这个问题其实很常见——当你在装饰器类的__call__方法里调用日志输出时,logging模块默认会从当前执行的栈帧(也就是Loggerator.__call__所在的上下文)提取filename、funcName这些属性,自然就会指向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.py,funcName会显示mult,lineno也会指向调用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_filename、biz_funcName等字段,好处是可以灵活控制要输出的信息,甚至同时输出装饰器和业务代码的上下文。
注意事项
- 方案一中的
stacklevel +1是因为你的装饰器只有一层,如果是多层嵌套装饰器,需要根据实际层数调整增量。 - 方案二中使用
inspect时,一定要记得在finally里删除frame引用,否则可能会造成内存泄漏。
内容的提问来源于stack exchange,提问作者Кирилл Фролов




