Wrapper 导致自定义 python 记录器中的 funcName 属性发生变化

Posted

技术标签:

【中文标题】Wrapper 导致自定义 python 记录器中的 funcName 属性发生变化【英文标题】:Wrapper causes change in the funcName attribute in custom python logger 【发布时间】:2017-10-25 03:56:42 【问题描述】:

我在自定义 python 记录器中创建了一个包装器,以在日志格式化程序中添加其他属性。

logFormat = logging.Formatter('[%(levelname)s],[%(asctime)-15s], %(API_SERVER)s, %(funcName)s,%(lineno)d, %(message)s')

附加属性 -> API_SERVER

日志将包装器方法显示为 funcName 而不是实际的 funcName。

[DEBUG],[2017-05-23 17:52:13,865], jupiter-api-server, loggingMethodsWrapper,91, Response returned from DolphinD is 200, returning status success, to caller.

包装代码-->

def loggingMethodsWrapper(self, logLevelMethod, *kargs):
    # Calling method logging methods dynamcially
    # Add the parameters in the extra if you want to add
    # more columns in logging format
    getattr(self.log, logLevelMethod)(
          *kargs, extra='API_SERVER': self.API_SERVER)

【问题讨论】:

【参考方案1】:

我有同样的问题,我有许多不同的功能,写了漂亮干净的日志包装器。在那种情况下,我当然需要反馈,它是什么功能。我复制了造成这种痛苦的source from github,只删除了几行

# enable overwritting log record attributes
# I know, dirty cheating



def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
               func=None, extra=None, sinfo=None):
"""
A factory method which can be overridden in subclasses to create
specialized LogRecords.
"""
rv = logging._logRecordFactory(name, level, fn, lno, msg, args, exc_info, func,
                               sinfo)
if extra is not None:
    for key in extra:
        rv.__dict__[key] = extra[key]
return rv


logging.Logger.makeRecord = makeRecord

比在 Logger 类中更改该方法的引用 所以现在我可以设置 LogRecord 的属性,即使它们已经被使用过,比如 funcName

所以包装看起来像这样

def log_string_convertors(convertor: callable):
    @wraps(convertor)
    def wrapper(string: str):
        result: str = convertor(string)
        logger.debug("%s->%s", string, result,
                 extra="input": string, "output": result, "funcName": convertor.__name__)
    return result

return wrapper

我将 LogRecords 保存到 json 以便以后能够用程序分析它们,所以我添加了一些额外的属性,例如输入和输出

【讨论】:

【参考方案2】:

您可以使用inspect 包来获取上一个函数的名称。这是一个显示“真实”调用者的日志记录功能的单例包装器示例:

import logging
import inspect

class Singleton(type):
    _instances = 

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class MyLogger(metaclass=Singleton):
    logger = None

    def __init__(self):
        logging.basicConfig(
            level=logging.INFO,
            format="%(asctime)s - %(threadName)s - %(message)s",
            handlers=[
                logging.StreamHandler()
            ])

        self.logger = logging.getLogger(__name__ + '.logger')

    @staticmethod
    def __get_call_info():
        stack = inspect.stack()

        # stack[1] gives previous function (info in our case)
        # stack[2] gives before previous function and so on

        fn = stack[2][1]
        ln = stack[2][2]
        func = stack[2][3]

        return fn, func, ln

    def info(self, message, *args):
        message = " -  at line : ".format(*self.__get_call_info(), message)
        self.logger.info(message, *args)

【讨论】:

【参考方案3】:

日志.py

class Log(Singleton):
log = None
caller = None
def __init__(self):
    ### Configure log
    # TODO: The config has to come from Dictconfig YAML file
    logFolder = CONSTANTS['LOG']['DIR']
    self.API_SERVER = API_SERVER
    caller = inspect.stack()[1][3]
    print caller
    self.log = logging.getLogger(__name__)

    # If handlers are already set donot do it again
    if not len(self.log.handlers):
        logFormat = logging.Formatter('[%(levelname)s],[%(asctime)-15s], %(API_SERVER)s, %(caller)s,%(lineno)d, %(message)s')

def loggingMethodsWrapper(self, logLevelMethod, *kargs):
    # Calling method logging methods dynamically
    # Add the parameters in the extra if you want to add
    # more columns in logging format

    getattr(self.log, logLevelMethod)(
          *kargs, extra='API_SERVER': self.API_SERVER, 'caller': self.caller)

示例 API 文件 -->

from flask import Flask, Response, request
from base import base
from jupiter_api.lib.jupFlaskClassy import *

from jupiter_api.business.accounts import accounts as bzAccounts
from jupiter_api.business.role_decorators import permGate, login_required
from flask.ext.cors import CORS
from jupiter_api.utils.log import Log
Log = Log()

class accounts(base):

    supported_methods = "OPTIONS, GET, POST, DELETE"
    ##  GET request to pull all user accounts
    @route('/<userId>/<companyId>/accounts/<accountId>', methods=['GET'])
    @permGate('list_accounts')
    @login_required
    def get_details(self, userId, companyId, accountId):
        #import pdb; pdb.set_trace()
        Log.debug("In get accounts for user")

        # Validate arguments
        if not userId or not companyId:
            return self.invalidData()

        # Print arguments
        Log.debug("Getting account details for user %s ", userId)

注意 - 我有 100 多个类似于调用 Log() 的 Accounts API 的 API。

【讨论】:

【参考方案4】:

发生了错误的函数名称,因为 loggingMethodsWrapper 是实际调用日志的函数。

你想要的似乎是调用 loggingMethodsWrapper 的函数。解决方法是更改​​您的格式化程序,使其不包含%(funcName)s,并将其作为传递给%(message)s 的参数的一部分或作为额外参数。

选项 1:

这样做,一个肮脏的解决办法是让你的包装器走上堆栈并抓住下一个函数。

在 python2 中这是这样的:

caller = inspect.stack()[1][3]

在 python3 上,这将是:

caller = inspect.stack()[1].filename

所以:

logFormat = logging.Formatter('[%(levelname)s],[%(asctime)-15s], %(API_SERVER)s, %(CALLER)s,%(lineno)d, %(message)s')

...

def loggingMethodsWrapper(self, logLevelMethod, *kargs):
    caller = # python-specific method of getting caller
    getattr(self.log, logLevelMethod)(
          *kargs, extra='CALLER' : caller, 'API_SERVER': self.API_SERVER)

选项 2:

如果您可以更改调用者,则简单修复 -- 将函数名称传递给您的记录器包装器

loggingMethodsWrapper(self, func_name, logLevelMethod, *kargs):
    ....

然后当你调用它时

def foo():
    loggingMethodsWrapper(foo.__name__, log_level, ...)

【讨论】:

loggingMethodWrapper() 是我的自定义 log.py 文件的一部分。我有 100 个调用 Log() 的 API 文件,但我不能在所有文件中调用 foo()。您能否建议一个不需要更改所有文件的替代方案? @xgord 当我调用 API(比如 Accounts)时,会调用 log()。日志文件中预期的 funcName 应该是 get_accounts,但它显示 loggingMethodsWrapper。 @TanuSachdeva 我用另一种方式更新了它。不过,如果您用一个如何调用log 以及如何设置log 的示例更新您的问题,这可能会有所帮助 也只是为了澄清第一条评论。我并不是说将函数 foo 添加到您的库中。这只是一个调用日志包装器的函数示例以及调用必须如何更改。 我已经尝试过选项 1,inspect.stack[1][3] 返回“None”。

以上是关于Wrapper 导致自定义 python 记录器中的 funcName 属性发生变化的主要内容,如果未能解决你的问题,请参考以下文章