Добавление переменного отступа перед каждой строкой вывода журнала Python

Я создаю вход в приложение 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

В настоящее время я работаю над своей документацией, обертывая методы класса этим декоратором:

import functools
import logging

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

Это лучше, чем ничего, но не совсем то, что я хочу. Как я могу обновить это (в идеале без использования глобальной переменной), чтобы иметь отступ переменной в начале каждой строки вывода журнала?

Эта идея кажется сомнительной. В некоторых случаях стек вызовов может быть очень глубоким (например, рекурсивные функции).

wim 30.07.2024 00:04

В этом случае оно никогда не зайдет очень глубоко, это приложение с графическим интерфейсом, поэтому всего несколько уровней в обработчиках событий и никакой рекурсии.

KBriggs 30.07.2024 02:04

Возможно, вам следует включить форматтеры. Средства форматирования Stdlib не имеют методов set_tabs/get_tabs.

wim 30.07.2024 04:31

У меня все заработало, скоро выложу хак - некрасиво, но работает

KBriggs 30.07.2024 14:07
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
4
71
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В итоге я использовал не совсем глобальную переменную, чтобы добиться этого, установив атрибут, направляющий logging.root:

        setattr(logging.root, 'indent', 0)
        setattr(logging.root, 'tab_spaces', 4)
        setattr(logging.root, 'extra', {'tabs': ' '*(0)})
        
        logging.basicConfig(level=logging.INFO, format = "%(tabs)%(asctime)s: %(levelname)s:s%(message)s")
        formatter = logging.Formatter("%%(tabs)(asctime)s: %(levelname)s:%(message)s", defaults = {'tabs': ''})
        
        root_logger = logging.getLogger()

        #console logger
        consoleHandler = logging.StreamHandler()
        consoleHandler.setFormatter(formatter)
        root_logger.addHandler(consoleHandler)

log_decorator теперь обновляет это свойство, чтобы поддерживать глобально доступную запись локального уровня отступа.

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

Теперь, когда вы захотите записать что-то в класс и соблюдать локальный уровень отступов, получите регистратор как переменную класса и вызовите его с аргументом extra.

def DummyClass():
    logger = logging.getLogger(__name__)
    ...
    logger.info('msg', extra=self.logger.root.extra)

Некрасиво, но работает и не выдаст ошибок, если вы решите не соблюдать отступы.

Другие вопросы по теме