python - 记录请求的旅程

python - log the request's journey

我想在请求结束时记录单个请求访问过的所有方法,以便进行调试。

我可以先从一个 class 开始:

这是我想要的输出示例:


logging full trace once
                      '__init__': ->
                                'init_method_1' ->
                                            'init_method_1_1' 
                                'init_method_2'
                      'main_function': ->
                                'first_main_function': ->
                                        'condition_method_3'
                                        'condition_method_5'

这是我的部分尝试:

import types

class DecoMeta(type):
    def __new__(cls, name, bases, attrs):

        for attr_name, attr_value in attrs.items():
            if isinstance(attr_value, types.FunctionType):
                attrs[attr_name] = cls.deco(attr_value)

        return super(DecoMeta, cls).__new__(cls, name, bases, attrs)

    @classmethod
    def deco(cls, func):
        def wrapper(*args, **kwargs):

            name = func.__name__
            stacktrace_full.setdefault(name, [])
            sorted_functions = stacktrace_full[name]
            if len(sorted_functions) > 0:
                stacktrace_full[name].append(name)
            result = func(*args, **kwargs)
            print("after",func.__name__)
            return result
        return wrapper

class MyKlass(metaclass=DecoMeta):

方法

我认为有两种不同的方法值得考虑解决这个问题:

  1. "Simple" 记录元class,或
  2. 更强大的元数据class 用于存储调用堆栈

如果您只需要打印方法调用,而不关心保存方法调用堆栈的实际记录,那么第一种方法应该可以解决问题。

我不确定您正在寻找哪种方法(如果您有任何具体的想法),但是如果您知道除了打印调用之外还需要存储方法调用堆栈,您可能想要向前跳到第二种方法。

注意:此后所有代码均假定存在以下导入:

from types import FunctionType

1。简单日志记录元class

这种方法要简单得多,并且不需要在您第一次尝试的基础上做太多额外的工作(取决于我们要考虑的特殊情况)。然而,正如已经提到的,这个 metaclass 只与日志有关。如果您确实需要保存方法调用堆栈结构,请考虑跳到第二种方法。

更改为 DecoMeta.__new__

使用这种方法,您的 DecoMeta.__new__ 方法基本保持不变。下面代码中最显着的变化是将“_in_progress_calls”列表添加到 namespaceDecoMeta.decowrapper 函数将使用此属性来跟踪已调用但未结束的方法数。有了这些信息,它就可以适当地缩进打印的方法名称。

还要注意将 staticmethod 包含到我们要通过 DecoMeta.deco 修饰的 namespace 属性中。但是,您可能不需要此功能。另一方面,您可能需要考虑进一步考虑 classmethod 和其他因素。

您会注意到的另一个变化是 cls 变量的创建,它在被 returned 之前直接修改。但是,您现有的通过命名空间的循环,然后是 class object 的创建和 return 仍然应该在这里起作用。

更改为 DecoMeta.deco

  1. 我们将in_progress_calls设置为当前实例的_in_progress_calls,稍后在wrapper

  2. 中使用
  3. 接下来,我们对您第一次尝试处理 staticmethod 进行小幅修改 — 如前所述,您可能想要也可能不想要

  4. 在“Log”部分,我们需要为下一行计算pad,我们在其中打印被调用方法的name。打印后,我们将当前方法name添加到in_progress_calls,通知其他方法in-progress方法

  5. 在“调用方法”部分,我们(可选)再次处理 staticmethod

    除了这个微小的变化之外,我们还进行了一个小而重要的变化,将 self 参数添加到我们的 func 调用中。没有这个,使用 DecoMeta 的 class 的普通方法将开始抱怨没有给出位置 self 参数,这是一个大问题,因为 func.__call__ 是a method-wrapper 并且需要我们的方法绑定到的实例。

  6. 对您第一次尝试的最后更改是删除最后一个 in_progress_calls 值,因为我们已经正式调用该方法并且正在 returning result

闭嘴,给我看代码

class DecoMeta(type):
    def __new__(mcs, name, bases, namespace):
        namespace["_in_progress_calls"] = []
        cls = super().__new__(mcs, name, bases, namespace)

        for attr_name, attr_value in namespace.items():
            if isinstance(attr_value, (FunctionType, staticmethod)):
                setattr(cls, attr_name, mcs.deco(attr_value))
        return cls

    @classmethod
    def deco(mcs, func):
        def wrapper(self, *args, **kwargs):
            in_progress_calls = getattr(self, "_in_progress_calls")

            try:
                name = func.__name__
            except AttributeError:  # Resolve `staticmethod` names
                name = func.__func__.__name__

            #################### Log ####################
            pad = " " * (len(in_progress_calls) * 3)
            print(f"{pad}`{name}`")
            in_progress_calls.append(name)

            #################### Invoke Method ####################
            try:
                result = func(self, *args, **kwargs)
            except TypeError:  # Properly invoke `staticmethod`-typed `func`
                result = func.__func__(*args, **kwargs)

            in_progress_calls.pop(-1)
            return result
        return wrapper

它有什么作用?

下面是一些虚拟 class 的代码,我试图根据您想要的示例输出进行建模:

设置

不要太在意这个街区。就是傻class他的方法调用其他方法

class MyKlass(metaclass=DecoMeta):
    def __init__(self):
        self.i_1()
        self.i_2()

    #################### Init Methods ####################
    def i_1(self):
        self.i_1_1()

    def i_1_1(self): ...
    def i_2(self): ...

    #################### Main Methods ####################
    def main(self, x):
        self.m_1(x)

    def m_1(self, x):
        if x == 0:
            self.c_1()
            self.c_2()
            self.c_4()
        elif x == 1:
            self.c_3()
            self.c_5()

    #################### Condition Methods ####################
    def c_1(self): ...
    def c_2(self): ...
    def c_3(self): ...
    def c_4(self): ...
    def c_5(self): ...

运行

my_k = MyKlass()
my_k.main(1)
my_k.main(0)

控制台输出

`__init__`
   `i_1`
      `i_1_1`
   `i_2`
`main`
   `m_1`
      `c_3`
      `c_5`
`main`
   `m_1`
      `c_1`
      `c_2`
      `c_4`

2。用于存储调用堆栈的 Beefy Metaclass

因为我不确定你是否真的想要这个,而且你的问题似乎更关注问题的元class部分,而不是调用堆栈存储结构,我将关注如何加强上述 metaclass 来处理所需的操作。然后,我将对存储调用堆栈的多种方法做一些说明,并使用简单的占位符结构“存根”代码的这些部分。

我们显然需要一个持久的调用堆栈结构来扩展临时 _in_progress_calls 属性的范围。因此,我们可以首先将以下未注释的行添加到 DecoMeta.__new__ 的顶部:

namespace["full_stack"] = dict()
# namespace["_in_progress_calls"] = []
# cls = super().__new__(mcs, name, bases, namespace)
# ...

不幸的是,显而易见的事情到此为止,如果您想跟踪非常简单的方法调用堆栈以外的任何东西,事情很快就会变得棘手。

关于我们需要如何保存调用堆栈,有几件事可能会限制我们的选择:

  1. 我们不能使用简单的字典,以方法名称作为键,因为在生成的 arbitrarily-complex 调用堆栈中,方法 X 完全有可能 cll 方法 Y 多次
  2. 我们不能假设每次调用方法 X 都会调用相同的方法,正如您使用“条件”方法的示例所示。这意味着我们不能说对 X 的任何调用都会产生调用堆栈 Y,并在某处巧妙地保存该信息
  3. 我们需要限制新 full_stack 属性的持久性,因为我们在 DecoMeta.__new__ 中基于 class-wide 声明它。如果我们不这样做,那么 MyKlass 的所有实例将共享相同的 full_stack,并迅速破坏其有用性

因为前两个高度依赖于你的 preferences/requirements 并且因为我认为你的问题更关心问题的元class 方面,而不是调用堆栈结构,我将从解决第三点。

为了确保每个实例都有自己的 full_stack,我们可以添加一个新的 DecoMeta.__call__ 方法,每当我们创建 MyKlass 的实例(或任何使用 DecoMeta 作为元class)。只需将以下内容放入 DecoMeta:

def __call__(cls, *args, **kwargs):
    setattr(cls, "full_stack", dict())
    return super().__call__(*args, **kwargs)

最后一块是弄清楚你想如何构造full_stack并添加代码以将其更新到DecoMeta.deco.wrapper函数。

一个deeply-nested字符串列表,按顺序命名调用的方法,以及这些方法调用的方法,等等......应该完成工作并回避上面提到的前两个问题, 但这听起来很乱,所以我会让你决定是否真的需要它。

例如,我们可以使full_stack成为一个字典,其键为Tuple[str],值为List[str]。请注意,在上述两种问题情况下,这将悄悄失败;但是,如果您决定更进一步,它确实可以说明 DecoMeta.deco.wrapper 所必需的更新。

只需添加两行:

首先,在 DecoMeta.deco.wrapper 的签名下方,添加以下未注释的行:

full_stack = getattr(self, "full_stack")
# in_progress_calls = getattr(self, "_in_progress_calls")
# ...

其次,在“日志”部分,在 print 调用之后,添加以下未注释的行:

# print(f"{pad}`{name}`")
full_stack.setdefault(tuple(in_progress_calls), []).append(name)
# in_progress_calls.append(name)
# ...

TL;DR

如果我将你的问题解释为要求一个真正只记录方法调用的元class是正确的,那么第一种方法(在上面“简单记录元class”下概述标题)应该很好用。但是,如果您还需要保存所有方法调用的完整记录,您可以按照“Beefy Metaclass to Store Call Stacks”标题下的建议开始。

如果您有任何其他问题或需要说明,请告诉我。我希望这有用!