在Python日志输出的每一行前面添加变量缩进

问题描述 投票:0回答:1

我正在将日志记录构建到一个Python应用程序中,我希望它是人类可读的。目前,调试日志记录了调用的每个函数以及参数和返回值。这意味着实际上,嵌套函数调用的调试日志可能如下所示:

2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,643: DEBUG: MainController.setup_connections called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
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

我想通过使用Python风格的缩进来嵌套事物,通过阅读日志来使这一点变得明显。所以理想的输出是这样的:

2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
    2024-07-29 16:52:26,643: DEBUG: MainController.setup_connections called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
    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

我目前正在通过使用此装饰器包装类方法来实现我的文档:

导入功能工具 导入日志记录

def log(_func=None, *, logger):
    def decorator_log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if logger.handlers:
                current_formatter = logger.handlers[0].formatter
                current_formatter.set_tabs(current_formatter.get_tabs() + 1)
            
            self = args[0]
            name = f'{self.__class__.__name__}.{func.__name__}'
            if logger.root.level < logging.DEBUG:
                logger.info(f"Entering {name}")
            else:
                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"{name} called with args {signature}")
                try:
                    result = func(*args, **kwargs)
                except Exception as e:
                    logger.exception(f"Exception raised in {name}: {str(e)}")
                    raise e
                if logger.root.level < logging.DEBUG:
                    logger.info(f"Leaving {name}")
                else:
                    logger.debug(f"{name} returned {result}")

                if logger.handlers:
                    current_formatter = logger.handlers[0].formatter
                    current_formatter.set_tabs(current_formatter.get_tabs() - 1)
                
                return result
        return wrapper
    if _func is None:
        return decorator_log
    else:
        return decorator_log(_func)

我可以使用

tabs
向记录器添加属性
setattr
并在装饰器的开头处递增/在装饰器末尾处递减,但这仅将选项卡应用于输出的
message
部分,如下所示:

2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,643: DEBUG:     MainController.setup_connections called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
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

这总比没有好,但不是我想要的。我如何更新它(最好不使用全局变量)以在记录器输出的每行开头有变量缩进?

python-3.x python-logging
1个回答
0
投票
import functools
import logging

# Custom formatter to handle indentation
class IndentFormatter(logging.Formatter):
    def __init__(self, fmt=None, datefmt=None, style='%', indent_char='    '):
        super().__init__(fmt, datefmt, style)
        self.indent_level = 0
        self.indent_char = indent_char
    
    def format(self, record):
        # Add the current level of indentation to the log message
        indent = self.indent_char * self.indent_level
        original_msg = super().format(record)
        indented_msg = "\n".join([indent + line for line in original_msg.split('\n')])
        return indented_msg
    
    def increase_indent(self):
        self.indent_level += 1
    
    def decrease_indent(self):
        if self.indent_level > 0:
            self.indent_level -= 1

# Decorator to log function calls with indentation
def log(_func=None, *, logger):
    def decorator_log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if logger.handlers:
                current_formatter = logger.handlers[0].formatter
                if isinstance(current_formatter, IndentFormatter):
                    current_formatter.increase_indent()
            
            self = args[0]
            name = f'{self.__class__.__name__}.{func.__name__}'
            if logger.root.level < logging.DEBUG:
                logger.info(f"Entering {name}")
            else:
                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"{name} called with args {signature}")
            try:
                result = func(*args, **kwargs)
            except Exception as e:
                logger.exception(f"Exception raised in {name}: {str(e)}")
                raise e
            if logger.root.level < logging.DEBUG:
                logger.info(f"Leaving {name}")
            else:
                logger.debug(f"{name} returned {result}")
                
            if logger.handlers:
                current_formatter = logger.handlers[0].formatter
                if isinstance(current_formatter, IndentFormatter):
                    current_formatter.decrease_indent()
                    
            return result
        return wrapper
    if _func is None:
        return decorator_log
    else:
        return decorator_log(_func)

# Example logger setup
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = IndentFormatter('%(asctime)s: %(levelname)s: %(message)s', '%Y-%m-%d %H:%M:%S')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

# Example class with decorated methods
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()
© www.soinside.com 2019 - 2024. All rights reserved.