【python修饰器】实现报log

A better way to logging in Python

python的通用日志装饰器

任何生产应用程序都可能有一些关于如何以及需要在您的应用程序中记录什么的指南。 更多时候,这些指南源于常见的行业模式,例如“记录所有异常”。 然而,实施这些准则留给了个人开发人员,并导致在整个代码库中重复相同的日志记录语句集。 例如,要记录所有异常,您将在每个 except 块中都有一个记录语句,用于捕获异常并将其记录在 ERROR 级别下。 但是,由于开发人员各自的开发风格,相同场景的日志记录语句可能会有所不同。 超时会导致应用程序中的日志记录碎片化和不一致。 此外,开发人员可能会犯错误并错过在必要的地方有日志记录。

缓解此问题的一种方法是利用 Python 的装饰器功能。 本文将简要概述装饰器,并演示如何创建装饰器来抽象这些常见的日志记录语句。 你可以阅读更多关于装饰器和它们可以在这个优秀的Primer on Python Decorators.

What are decorators

装饰器是一个函数,它接受另一个函数并扩展它的行为而不显式修改它。 这些也被称为 higher-order functions.

Python的函数是 first-class citizens. 这意味着函数可以作为参数传递,也可以作为赋值的对象。 因此,如果您有一个函数 def sum(a, b=10),您可以将它用作任何其他对象并深入了解它的属性。

def sum(a, b=10):
    return a+b
>>> sum
<function sum at 0x7f35e9dde310>
>>> sum.__code__.co_varnames  # Names of local variables
('a', 'b')

由于函数的行为类似于对象,因此您可以将“sum”分配给另一个函数。 然后调用 sum 将调用另一个函数,而不是我们之前定义的函数。 装饰器通过为 sum 分配一个新函数来利用这种行为,该函数以 sum 作为参数并围绕它包装一些额外的逻辑,从而在不修改函数本身的情况下 扩展 它。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # do something before `sum`
        result = func(*args, **kwargs)
        # do something after `sum`
        return result
    return wrapper

sum = my_decorator(sum)
>>> sum
<function my_decorator.<locals>.wrapper at 0x7f9c0359b0d0>

这种模式非常普遍,以至于 Python 有一个语法糖来装饰一个函数。 因此,我们可以在 sum 方法之上使用 @ 符号,而不是 sum = my_decorator(sum),如下所示 -

@my_decorator # Equivalent to `sum = my_decorator(sum)` after the method
def sum(a, b=10):
    return a+b

Logging Decorator

我们将创建一个装饰器来处理两种常见的日志记录场景 - 将异常记录为 ERROR 并将方法参数记录为 DEBUG 日志。

让我们首先捕获异常并使用 python logging 库记录它。

import functools
import logging

logging.basicConfig(level = logging.DEBUG)
logger = logging.getLogger()

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            result = func(*args, **kwargs)
            return result
        except Exception as e:
            logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
            raise e
    return wrapper

@log
def foo():
    raise Exception("Something went wrong")
ERROR:root:Exception raised in foo. exception: Something went wrong
Traceback (most recent call last):
  File "<REDACTED>/foo.py", line 15, in wrapper
    result = func(*args, **kwargs)
  File "<REDACTED>/foo.py", line 28, in foo
    raise Exception("Something went wrong")
Exception: Something went wrong

除了设置 logger,我们还使用了 @functools.wraps 装饰器. wraps 装饰器将 wrapper 函数更新为看起来像 func。 我们的 @log 装饰器现在可以用于任何函数,以从 wrapped 函数中捕获每个异常并以一致的方式记录它。

由于 wrapper 函数接受所有参数(*args 和 **kwargs),所以可以扩展 @log 装饰器以捕获传递给装饰函数的所有参数。 我们可以通过迭代 argskwargs 并加入它们以形成要记录的字符串消息来做到这一点。

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        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"function {func.__name__} called with args {signature}")
        try:
            result = func(*args, **kwargs)
            return result
        except Exception as e:
            logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
            raise e
    return wrapper
>>> sum(10, b=20)
DEBUG:root:function sum called with args 10, b=20
30

我们在 DEBUG 级别记录参数,因为我们不希望我们的日志中包含所有函数参数。可以在必要时在我们的系统上切换调试日志记录。 请记住,这会将所有参数值写入日志,包括任何 PII 数据或机密

这个基本的日志装饰器看起来不错,并且已经完成了我们最初设定的目标。只要一个方法用 @log 装饰器装饰,我们将记录其中引发的任何异常以及传递给它的所有参数。

然而,在实际项目中,logger 本身可以被抽象为它自己的类,该类基于某些配置(例如将日志推送到云接收器)初始化记录器。在这种情况下,通过在 @log 装饰器中创建我们自己的记录器来登录控制台是没有用的。我们需要一种在运行时将现有的 logger 传递给我们的装饰器的方法。为此,我们可以扩展 @log 装饰器以接受 logger 作为参数。

为了模拟这种情况,我们将首先创建一个为我们创建创建记录器的类。现在我们将创建基本的记录器,但您可以想象该类根据需要配置记录器的行为。

class MyLogger:
    def __init__(self):
        logging.basicConfig(level=logging.DEBUG)

    def get_logger(self, name=None):
        return logging.getLogger(name)

由于在编写装饰器时我们不知道底层函数是否会传递给我们 MyLoggerlogging.logger 或根本没有记录器,所以我们的通用装饰器将能够处理所有这些。

from typing import Union

def get_default_logger():
    return MyLogger().get_logger()

def log(_func=None, *, my_logger: Union[MyLogger, logging.Logger] = None):
    def decorator_log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if my_logger is None:
                logger = get_default_logger()
            else:
                if isinstance(my_logger, MyLogger):
                    logger = my_logger.get_logger(func.__name__)
                else:
                    logger = my_logger
            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"function {func.__name__} called with args {signature}")
            try:
                result = func(*args, **kwargs)
                return result
            except Exception as e:
                logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
                raise e
        return wrapper

    if _func is None:
        return decorator_log
    else:
        return decorator_log(_func)

上面的代码看起来很吓人,但让我总结一下。 @log 装饰器现在处理三种不同的场景 -

  • 没有记录器通过:这是我们在此之前一直在做的相同场景。 装饰器仅用作函数顶部的“@log”语句。 在这种情况下,装饰器通过调用 get_default_logger 方法获取一个记录器,并将其用于该方法的其余部分。
  • MyLogger 已通过:我们的 @log 装饰器现在可以接受 MyLogger 的实例作为参数。 然后它可以调用 MyLogger.get_logger 方法来创建一个嵌套的记录器,并在剩下的时间里使用它。
@log(my_logger=MyLogger())
def sum(a, b=10):
    return a + b
  • logging.logger 被传递:在第三种情况下,我们可以传递记录器本身而不是传递 MyLogger 类。
lg = MyLogger().get_logger()

@log(my_logger=lg)
def sum(a, b=10):
    return a + b

我们还没有完成。即使在当前形式下,我们的日志装饰器也受到限制。一个限制是我们必须在我们想要装饰的方法之前*有 loggerMyLogger 可用。换句话说,对 logger must 的引用必须在方法本身存在之前存在。这可能适用于目标函数是类的一部分并且类 __init__ 方法可以实例化记录器的情况,但它不适用于类上下文之外的函数。在许多现实世界的应用程序中,我们不会让每个模块或函数创建自己的记录器。相反,我们可能希望将记录器传递给函数本身。 loggerMyLogger 将依赖注入到下游方法中。换句话说,一个函数可能在其参数中将记录器传递给它。

但是如果函数是类的一部分,那么 logger 将被注入到类本身而不是类的每个方法中。在这种情况下,我们希望使用类可用的记录器。

所以我们的目标是捕获作为参数传递给修饰函数或**传递给我们修饰函数的类构造函数logger,并使用它来记录来自修饰器*本身的日志。通过这样做,我们的装饰器可以与记录器本身完全解耦,并将利用运行时底层方法可用的任何记录器。

为此,我们将遍历 argskwargs 参数并检查是否在其中任何一个中获得了 logger。要检查函数是否是类的一部分,我们可以检查 args 的第一个参数是否具有属性 __dict__。如果第一个参数具有属性 __dict__,我们将遍历 __dict__.values() 并检查其中一个值是否是我们的记录器。最后,如果没有任何效果,我们将默认使用 get_default_logger 方法。

def log(_func=None, *, my_logger: Union[MyLogger, logging.Logger] = None):
    def decorator_log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            logger = get_default_logger()
            try:
                if my_logger is None:
                    first_args = next(iter(args), None)  # capture first arg to check for `self`
                    logger_params = [  # does kwargs have any logger
                        x
                        for x in kwargs.values()
                        if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
                    ] + [  # # does args have any logger
                        x
                        for x in args
                        if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
                    ]
                    if hasattr(first_args, "__dict__"):  # is first argument `self`
                        logger_params = logger_params + [
                            x
                            for x in first_args.__dict__.values()  # does class (dict) members have any logger
                            if isinstance(x, logging.Logger)
                            or isinstance(x, MyLogger)
                        ]
                    h_logger = next(iter(logger_params), MyLogger())  # get the next/first/default logger
                else:
                    h_logger = my_logger  # logger is passed explicitly to the decorator

                if isinstance(h_logger, MyLogger):
                    logger = h_logger.get_logger(func.__name__)
                else:
                    logger = h_logger

                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"function {func.__name__} called with args {signature}")
            except Exception:
                pass

            try:
                result = func(*args, **kwargs)
                return result
            except Exception as e:
                logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
                raise e
        return wrapper

    if _func is None:
        return decorator_log
    else:
        return decorator_log(_func)

上面的装饰器足够通用,除了我们之前讨论的 3 个场景之外,还可以用于另外 2 个场景 -

  • loggerMyLogger 被传递给装饰方法
@log
def foo(a, b, logger):
    pass

@log
def bar(a, b=10, logger=None): # Named parameter
    pass

foo(10, 20, MyLogger())  # OR foo(10, 20, MyLogger().get_logger())
bar(10, b=20, logger=MyLogger())  # OR bar(10, b=20, logger=MyLogger().get_logger())
  • loggerMyLogger 被传递给托管装饰函数的类 __init__ 方法
class Foo:
    def __init__(self, logger):
        self.lg = logger

    @log
    def sum(self, a, b=10):
        return a + b

Foo(MyLogger()).sum(10, b=20)  # OR Foo(MyLogger().get_logger()).sum(10, b=20)

我们所做的另一件事是在调用修饰函数 func 之前将所有代码包装在一个try - except块中。 即使在调用目标函数之前,我们也不希望由于日志记录问题而导致执行失败。 在任何情况下,我们的日志记录逻辑都不应该导致系统故障。

Conclusion

上面的装饰器是一个很好的起点,可以根据需要进行扩展或简化。 它减少了错过异常日志记录的机会,并标准化了整个应用程序的错误消息。

如果您有任何问题,请在下面的评论中与我联系。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值