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 属性发生变化的主要内容,如果未能解决你的问题,请参考以下文章

mybatis plus wrapper.orderBy 自定义排序 自定义sql 多字段拼接

mybatis plus wrapper.orderBy 自定义排序 自定义sql 多字段拼接

mybatis plus wrapper.orderBy 自定义排序 自定义sql 多字段拼接

MybatisPlus条件构造器Wrapper分页查询自定义SQLService层接口代码生成器

Python日志记录使用自定义格式加密

python ArgParse 自定义操作与使用 metavar 的键值选项导致 -h 输出中的重复