Подтвердить что ты не робот

Отображение правильного имени funcName при использовании функции логгера в пользовательском классе

Это строка форматирования, которую я использую для ведения журнала:

'%(asctime)s - %(levelname)-10s - %(funcName)s - %(message)s'

Но чтобы показать сообщения о регистрации, у меня есть обертка, делающая немного больше (я настраиваю разные уровни журналов, настраиваю различные бэкэнды ведения журнала, предоставляю удобные функции для доступа к пользовательским уровням и т.д.):

class MyLogger(logging.Logger):

    def split_line(self, level, message):
        ....
        self.log.(level, line)

    def progress(self, message):
        self.split_line(PROGRESS, message)

С этой настройкой всякий раз, когда я записываю что-то:

def myfunc():
    log.progress('Hello')

Я получаю:

013-10-27 08:47:30,130 - PROGRESS   - split_line - Hello

Это не то, что я хотел, а именно:

013-10-27 08:47:30,130 - PROGRESS   - myfunc     - Hello

Как я могу сказать регистратору использовать правильный контекст для имени функции? Я думаю, что это будет на самом деле на два уровня выше на стеке.

ИЗМЕНИТЬ

Это тестовая программа, показывающая проблему:

import sys
import logging

PROGRESS = 1000

class MyLogger(logging.Logger):

    PROGRESS = PROGRESS
    LOG_FORMATTER = '%(asctime)s - %(levelname)-10s - %(funcName)s - %(message)s'
    DEF_LOGGING_LEVEL = logging.WARNING

    def __init__(self, log_name, level=None):
        logging.Logger.__init__(self, log_name)
        self.formatter = logging.Formatter(self.LOG_FORMATTER)
        self.initLogger(level)

    def initLogger(self, level=None):
        self.setLevel(level or self.DEF_LOGGING_LEVEL)
        self.propagate = False

    def add_handler(self, log_file, use_syslog):
        if use_syslog : hdlr = logging.handlers.SysLogHandler(address='/dev/log')
        elif log_file : hdlr = logging.FileHandler(log_file)
        else          : hdlr = logging.StreamHandler(sys.stderr)
        hdlr.setFormatter(self.formatter)
        self.addHandler(hdlr)
        return hdlr

    def addHandlers(self, log_file=None, progress_file=None, use_syslog=False):
        self.logger_hdlr = self.add_handler(log_file, use_syslog)
        if progress_file:
            self.progress_hdlr = self.add_handler(progress_file, use_syslog)
            self.progress_hdlr.setLevel(self.PROGRESS)
        else:
            self.progress_hdlr = None

    def split_line(self, level, txt, *args):
        txt = txt % (args)
        for line in txt.split('\n'):
            self.log(level, line)

    def progress(self, txt, *args):
        self.split_line(self.PROGRESS, txt, *args)

logging.setLoggerClass(MyLogger)
logging.addLevelName(PROGRESS, 'PROGRESS')
logger = logging.getLogger(__name__)
logger.addHandlers()

name = 'John'
logger.progress('Hello %s\nHow are you doing?', name)

Выдает:

2013-10-27 09:47:39,577 - PROGRESS   - split_line - Hello John
2013-10-27 09:47:39,577 - PROGRESS   - split_line - How are you doing?
4b9b3361

Ответ 1

По существу, код, который виноват, лежит в классе Logger:

Этот метод

def findCaller(self):
    """
    Find the stack frame of the caller so that we can note the source
    file name, line number and function name.
    """
    f = currentframe()
    #On some versions of IronPython, currentframe() returns None if
    #IronPython isn't run with -X:Frames.
    if f is not None:
        f = f.f_back
    rv = "(unknown file)", 0, "(unknown function)"
    while hasattr(f, "f_code"):
        co = f.f_code
        filename = os.path.normcase(co.co_filename)
        if filename == _srcfile:
            f = f.f_back
            continue
        rv = (co.co_filename, f.f_lineno, co.co_name)
        break
    return rv

возвращает первую функцию в цепочке вызывающих, которая не принадлежит текущему модулю.

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


В вашем особом случае, вероятно, будет проще воздержаться от автоматического разделения линии и сделать

logger.progress('Hello %s', name)
logger.progress('How are you doing?')

или сделать

def splitter(txt, *args)
    txt = txt % (args)
    for line in txt.split('\n'):
        yield line

for line in splitter('Hello %s\nHow are you doing?', name):
    logger.progress(line)

и иметь

def progress(self, txt, *args):
    self.log(self.PROGRESS, txt, *args)

Вероятно, это сэкономит вам много головной боли.

ИЗМЕНИТЬ 2: Нет, это не поможет. Теперь он покажет вам progress как имя вашей вызывающей функции...

Ответ 2

Спасибо @cygnusb и другим, которые уже предоставили полезные указатели. Я решил использовать метод Python 3.4 Logger.findCaller в качестве отправной точки. Следующее решение было протестировано с Python 2.7.9 и 3.4.2. Этот код предназначен для размещения в его собственном модуле. Он дает правильный ответ только с одной итерацией цикла.

import io
import sys

def _DummyFn(*args, **kwargs):
    """Placeholder function.

    Raises:
        NotImplementedError
    """
    _, _ = args, kwargs
    raise NotImplementedError()

# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame, by skipping frames whose filename is that of this
# module source. It therefore should contain the filename of this module's
# source file.
_srcfile = os.path.normcase(_DummyFn.__code__.co_filename)
if hasattr(sys, '_getframe'):
    def currentframe():
        return sys._getframe(3)
else:  # pragma: no cover
    def currentframe():
        """Return the frame object for the caller stack frame."""
        try:
            raise Exception
        except Exception:
            return sys.exc_info()[2].tb_frame.f_back

class WrappedLogger(logging.Logger):
    """Report context of the caller of the function that issues a logging call.

    That is, if

        A() -> B() -> logging.info()

    Then references to "%(funcName)s", for example, will use A context
    rather than B context.

    Usage:
        logging.setLoggerClass(WrappedLogger)
        wrapped_logging = logging.getLogger("wrapped_logging")
    """
    def findCaller(self, stack_info=False):
        """Return the context of the caller parent.

        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.

        This is based on the standard python 3.4 Logger.findCaller method.
        """
        sinfo = None
        f = currentframe()
        # On some versions of IronPython, currentframe() returns None if
        # IronPython isn't run with -X:Frames.
        if f is not None:
            f = f.f_back

        if sys.version_info.major == 2:
            rv = "(unknown file)", 0, "(unknown function)"
        else:
            rv = "(unknown file)", 0, "(unknown function)", sinfo

        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == _srcfile or filename == logging._srcfile:
                f = f.f_back
                continue
            # We want the frame of the caller of the wrapped logging function.
            # So jump back one more frame.
            f = f.f_back
            co = f.f_code
            if sys.version_info.major == 2:
            rv = "(unknown file)", 0, "(unknown function)"
        else:
            rv = "(unknown file)", 0, "(unknown function)", sinfo

        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename == _srcfile or filename == logging._srcfile:
                f = f.f_back
                continue
            # We want the frame of the caller of the wrapped logging function.
            # So jump back one more frame.
            f = f.f_back
            co = f.f_code
            if sys.version_info.major == 2:
                rv = co.co_filename, f.f_lineno, co.co_name
            else:
                if stack_info:
                    sio = io.StringIO()
                    sio.write('Stack (most recent call last):\n')
                    traceback.print_stack(f, file=sio)
                    sinfo = sio.getvalue()
                    if sinfo[-1] == '\n':
                        sinfo = sinfo[:-1]
                    sio.close()
                rv = co.co_filename, f.f_lineno, co.co_name, sinfo
            break

        return rv

Ответ 3

Прежде всего, согласно вашему коду, понятно, почему это происходит, levelname и funcName "принадлежит" self.log, поэтому, когда вы вызываете self.log(level, line), levelname есть level и funcName line.

У вас есть 2 варианта ИМХО:

  • Чтобы использовать модуль inspect, чтобы получить текущий метод и доставить его внутри сообщения, вы можете проанализировать его и использовать его очень легко.

  • Лучшим подходом будет использование inspect внутри split_line для получения метода "отец" вы можете изменить число (3) в следующем коде, чтобы "играть" с иерархией методов.

пример использования метода проверки для получения текущего метода

from inspect import stack

class Foo:
    def __init__(self):
        print stack()[0][3]

f = Foo()

Ответ 4

Вы можете объединить метод progress и метод split_line:

def progress(self, txt, *args, **kwargs):
    if self.isEnabledFor(self.PROGRESS):
        txt = txt % (args)
        for line in txt.split('\n'):
            self._log(self.PROGRESS, line, [], **kwargs)

Ответ 5

Как было предложено в первом ответе, подклассификация класса Logger и использование logging.setLoggerClass должны сделать трюк. Вам понадобится модифицированная функция findCaller, которая обрабатывает ваши завернутые функциональные вызовы.

Поместите следующее в модуль, так как метод класса findCaller выполняет поиск первого вызова из файла, который не является текущим исходным именем файла.

import inspect
import logging
import os

if hasattr(sys, 'frozen'): #support for py2exe
    _srcfile = "logging%s__init__%s" % (os.sep, __file__[-4:])
elif __file__[-4:].lower() in ['.pyc', '.pyo']:
    _srcfile = __file__[:-4] + '.py'
else:
    _srcfile = __file__
_srcfile = os.path.normcase(_srcfile)

class WrappedLogger(logging.Logger):
    def __init__(self,name):
        logging.Logger.__init__(self, name)

    def findCaller(self):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.
        """
        # get all outer frames starting from the current frame
        outer = inspect.getouterframes(inspect.currentframe())
        # reverse the order, to search from out inward
        outer.reverse()
        rv = "(unknown file)", 0, "(unknown function)"    

        pos = 0
        # go through all frames
        for i in range(0,len(outer)):
            # stop if we find the current source filename
            if outer[i][1] == _srcfile:
                # the caller is the previous one
                pos=i-1
                break

        # get the frame (stored in first tuple entry)
        f = outer[pos][0]

        co = f.f_code
        rv = (co.co_filename, f.f_lineno, co.co_name)

        return rv
# Usage:
logging.setLoggerClass(WrappedLogger)
log = logging.getLogger("something")

Ответ 6

Кто-то дал правильный ответ. Я сделаю резюме.

logging.Logger.findCaller(), он фильтрует фреймы стека с помощью logging._srcfile в оригинальном пакете logging.

Таким образом, мы делаем то же самое, фильтруем нашу собственную оболочку логгера my_log_module._srcfile. Мы динамически заменяем метод logging.Logger.findCaller() вашего экземпляра регистратора.

Кстати, пожалуйста, не создавайте подкласс logging.Logger, пакет logging не предназначен для ООП, когда findCaller, pitty... да?

# file: my_log_module.py, Python-2.7, define your logging wrapper here
import sys
import os
import logging
my_logger = logging.getLogger('my_log')

if hasattr(sys, '_getframe'): currentframe = lambda: sys._getframe(3)
# done filching

#
# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame.
#
_srcfile = os.path.normcase(currentframe.__code__.co_filename)

def findCallerPatch(self, *args):
    """
    Find the stack frame of the caller so that we can note the source
    file name, line number and function name.
    """
    f = currentframe()
    #On some versions of IronPython, currentframe() returns None if
    #IronPython isn't run with -X:Frames.
    if f is not None:
        f = f.f_back
    rv = "(unknown file)", 0, "(unknown function)"
    while hasattr(f, "f_code"):
        co = f.f_code
        filename = os.path.normcase(co.co_filename)
        if filename == _srcfile:
            f = f.f_back
            continue
        rv = (co.co_filename, f.f_lineno, co.co_name)
        break
    return rv

# DO patch
my_logger.findCaller = findCallerPatch

Хорошо, все готово. Теперь вы можете использовать свой регистратор в других модулях, добавить формат сообщения регистрации: lino, путь, имя метода, blablabla

# file: app.py
from my_log_module import my_logger
my_logger.debug('I can check right caller now')

Или вы можете использовать элегантный способ, но не используйте глобальный logging.setLoggerClass

# file: my_log_modue.py
import logging
my_logger = logging.getLogger('my_log')

class MyLogger(logging.Logger):
    ...

my_logger.__class__ = MyLogger

Ответ 7

Благодаря @glglgl я мог придумать продвинутый поиск findCaller

Обратите внимание, что инициализация _logging_srcfile и _this_srcfile - вдохновлена ​​исходным кодом протокола python

Конечно, вы можете поместить свои собственные правила в findCaller() - здесь я просто исключаю все из файла, где находится пользовательский регистратор, ИСКЛЮЧАЕТ функцию test_logging.

ВАЖНО пользовательский регистратор извлекается только при передаче имени в getLogger(name) factory. Если вы просто выполните logging.getLogger(), вы получите RootLogger, который не является вашим регистратором.

import sys
import os
import logging
# from inspect import currentframe
currentframe = lambda: sys._getframe(3)
_logging_srcfile = os.path.normcase(logging.addLevelName.__code__.co_filename)
_this_srcfile = __file__


def test_logging():
    logger = logging.getLogger('test')
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(logging.Formatter('%(funcName)s: %(message)s'))
    handler.setLevel(11)
    logger.addHandler(handler)
    logger.debug('Will not print')
    logger.your_function('Test Me')


class CustomLogger(logging.getLoggerClass()):
    def __init__(self, name, level=logging.NOTSET):
        super(CustomLogger, self).__init__(name, level)

    def your_function(self, msg, *args, **kwargs):
        # whatever you want to do here...
        self._log(12, msg, args, **kwargs)

    def findCaller(self):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.

        This function comes straight from the original python one
        """
        f = currentframe()
        # On some versions of IronPython, currentframe() returns None if
        # IronPython isn't run with -X:Frames.
        if f is not None:
            f = f.f_back
        rv = "(unknown file)", 0, "(unknown function)"
        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            ## original condition
            # if filename == _logging_srcfile:
            ## PUT HERE YOUR CUSTOM CONDITION, eg:
            ## skip also this file, except the test_logging method which is used for debug
            if co.co_name != 'test_logging' and filename in [_logging_srcfile, _this_srcfile]:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

logging.setLoggerClass(CustomLogger)