如何为Python日志输出的每行添加可变缩进(非全局变量实现)
实现Python嵌套函数调用的全行列缩进日志
核心思路
要实现全行列缩进,关键是在日志格式化阶段给整条日志内容添加前缀缩进,同时用线程局部存储(threading.local)跟踪当前调用栈的缩进级别,避免全局变量带来的线程安全问题。
实现步骤
1. 自定义支持缩进的日志格式化器
创建继承自logging.Formatter的自定义类,用threading.local存储每个线程独立的缩进级别,在格式化日志时给整条内容添加对应缩进:
import logging import threading class IndentFormatter(logging.Formatter): def __init__(self, fmt=None, datefmt=None, style='%'): super().__init__(fmt, datefmt, style) self._local = threading.local() self._local.indent_level = 0 # 初始化缩进级别为0 @property def indent_level(self): return getattr(self._local, 'indent_level', 0) @indent_level.setter def indent_level(self, value): self._local.indent_level = max(0, value) # 避免级别为负数 def format(self, record): # 先获取原始格式化后的日志内容 original_output = super().format(record) # 生成对应级别的缩进空格(这里用4个空格,可自定义) indent = ' ' * self.indent_level # 给整条日志添加缩进前缀 return f"{indent}{original_output}"
2. 修改日志装饰器,正确管理缩进级别
调整装饰器逻辑,确保进入函数时递增缩进,退出时(包括异常场景)递减缩进,同时使用logger.isEnabledFor()替代原有的root级别判断,更准确:
import functools import logging def log(_func=None, *, logger): def decorator_log(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 找到当前logger使用的IndentFormatter实例 indent_formatter = None for handler in logger.handlers: if isinstance(handler.formatter, IndentFormatter): indent_formatter = handler.formatter break # 进入函数时递增缩进 if indent_formatter: indent_formatter.indent_level += 1 self = args[0] func_full_name = f'{self.__class__.__name__}.{func.__name__}' try: # 记录函数调用日志 if logger.isEnabledFor(logging.DEBUG): args_repr = [repr(a) for a in args] kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] signature = ", ".join(args_repr + kwargs_repr) logger.debug(f"{func_full_name} called with args {signature}") else: logger.info(f"Entering {func_full_name}") # 执行原函数 result = func(*args, **kwargs) # 记录函数返回日志 if logger.isEnabledFor(logging.DEBUG): logger.debug(f"{func_full_name} returned {result!r}") else: logger.info(f"Leaving {func_full_name}") return result except Exception as e: logger.exception(f"Exception raised in {func_full_name}: {str(e)}") raise finally: # 无论是否异常,退出函数时递减缩进 if indent_formatter: indent_formatter.indent_level -= 1 return wrapper if _func is None: return decorator_log else: return decorator_log(_func)
3. 配置日志并使用装饰器
初始化logger时绑定自定义的IndentFormatter,然后给需要日志的类方法添加装饰器:
# 配置日志 logger = logging.getLogger("app_logger") logger.setLevel(logging.DEBUG) # 创建控制台Handler并绑定缩进格式化器 console_handler = logging.StreamHandler() log_format = "%(asctime)s: %(levelname)s: %(message)s" console_handler.setFormatter(IndentFormatter(log_format)) logger.addHandler(console_handler) # 测试类 class MainController: @log(logger=logger) def initialize_components(self): self.setup_connections() @log(logger=logger) def setup_connections(self): pass # 运行测试 if __name__ == "__main__": controller = MainController() controller.initialize_components()
效果验证
运行上述代码后,日志输出会和预期一致:
2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <__main__.MainController object at 0x102345678> 2024-07-29 16:52:26,643: DEBUG: MainController.setup_connections called with args <__main__.MainController object at 0x102345678> 2024-07-29 16:52:26,645: DEBUG: MainController.setup_connections returned None 2024-07-29 16:52:26,646: DEBUG: MainController.initialize_components returned None
关键优势
- 线程安全:用
threading.local存储缩进级别,每个线程的调用栈缩进独立,不会互相干扰 - 无全局变量:所有状态封装在格式化器实例中,避免全局变量的副作用
- 全行列缩进:通过格式化器修改整条日志内容,实现时间、级别、消息的统一缩进
- 异常安全:使用
finally块确保缩进级别在函数退出时总能正确恢复
内容的提问来源于stack exchange,提问作者KBriggs
相关产品推荐
相关产品推荐

