You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 07:17:11