简体   繁体   English

日志中的自定义记录器类和正确的行号/函数名称

[英]Custom Logger class and correct line number/function name in log

I'd like to wrap Python logger in a custom class to embed some application-specific functionality and hide setup details from developers (setting file output, logging level, etc).我想将 Python 记录器包装在自定义类中以嵌入一些特定于应用程序的功能并向开发人员隐藏设置细节(设置文件输出、日志记录级别等)。 To do this, I created a class with the following API:为此,我使用以下 API 创建了一个类:

__init__(log_level, filename)
debug(msg)
info(msg)
warning(msg)
error(msg)

Logger.debug/info/warning/etc calls usually write in the log the function and line number where the log call was made. Logger.debug/info/warning/etc 调用通常在日志中写入进行日志调用的函数和行号。 However, using my custom class, the function and line numbers written to the log file are always the same (correspondent to the debug()/info()/warning()/error() functions inside the custom class).但是,使用我的自定义类,写入日志文件的函数和行号始终相同(对应于自定义类内部的 debug()/info()/warning()/error() 函数)。 I want it to save the line of application code that logged the msg.我希望它保存记录消息的应用程序代码行。 Is that possible?那可能吗?

Thanks in advance.提前致谢。

It is possible to generate a log wrapper if you are willing to re-implent a little bit of the standard logging module.如果您愿意重新实现一些标准日志记录模块,则可以生成日志包装器。 The trick is to write your own findCaller() method that knows how to ignore your logging-wrapper source-file when interpreting back-traces.诀窍是编写您自己的 findCaller() 方法,该方法知道在解释回溯时如何忽略您的日志包装源文件。

in logwrapper.py:在 logwrapper.py 中:

import logging
import os
import sys

from logging import *


# This code is mainly copied from the python logging module, with minor modifications

# _srcfile is used when walking the stack to check when we've got the first
# caller stack frame.
#
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 LogWrapper(object):
    def __init__(self, logger):
        self.logger = logger

    def debug(self, msg, *args, **kwargs):
        """
        Log 'msg % args' with severity 'DEBUG'.

        To pass exception information, use the keyword argument exc_info with
        a true value, e.g.

        logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
        """
        if self.logger.isEnabledFor(DEBUG):
            self._log(DEBUG, msg, args, **kwargs)

    def info(self, msg, *args, **kwargs):
        """
        Log 'msg % args' with severity 'INFO'.

        To pass exception information, use the keyword argument exc_info with
        a true value, e.g.

        logger.info("Houston, we have a %s", "interesting problem", exc_info=1)
        """
        if self.logger.isEnabledFor(INFO):
            self._log(INFO, msg, args, **kwargs)


    # Add other convenience methods

    def log(self, level, msg, *args, **kwargs):
        """
        Log 'msg % args' with the integer severity 'level'.

        To pass exception information, use the keyword argument exc_info with
        a true value, e.g.

        logger.log(level, "We have a %s", "mysterious problem", exc_info=1)
        """
        if not isinstance(level, int):
            if logging.raiseExceptions:
                raise TypeError("level must be an integer")
            else:
                return
        if self.logger.isEnabledFor(level):
            self._log(level, msg, args, **kwargs)


    def _log(self, level, msg, args, exc_info=None, extra=None):
        """
        Low-level logging routine which creates a LogRecord and then calls
        all the handlers of this logger to handle the record.
        """
        # Add wrapping functionality here.
        if _srcfile:
            #IronPython doesn't track Python frames, so findCaller throws an
            #exception on some versions of IronPython. We trap it here so that
            #IronPython can use logging.
            try:
                fn, lno, func = self.findCaller()
            except ValueError:
                fn, lno, func = "(unknown file)", 0, "(unknown function)"
        else:
            fn, lno, func = "(unknown file)", 0, "(unknown function)"
        if exc_info:
            if not isinstance(exc_info, tuple):
                exc_info = sys.exc_info()
        record = self.logger.makeRecord(
            self.logger.name, level, fn, lno, msg, args, exc_info, func, extra)
        self.logger.handle(record)


    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 = logging.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

And an example of using it:以及使用它的示例:

import logging
from logwrapper import LogWrapper

logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(filename)s(%(lineno)d): "
                    "%(message)s")
logger = logging.getLogger()
lw = LogWrapper(logger)

lw.info('Wrapped well, this is interesting')

Based on @Will Ware's answer.基于@Will Ware 的回答。 Another option is to overwrite findCaller method and use custom class as default logger:另一种选择是覆盖findCaller方法并使用自定义类作为默认记录器:

class MyLogger(logging.Logger):
    """
        Needs to produce correct line numbers
    """
    def findCaller(self):
        n_frames_upper = 2
        f = logging.currentframe()
        for _ in range(2 + n_frames_upper):  # <-- correct frame
            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 == logging._srcfile:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

logging.setLoggerClass(MyLogger)
logger = logging.getLogger('MyLogger')  # init MyLogger
logging.setLoggerClass(logging.Logger) # reset logger class to default

Yes: sys._getframe(NUM) where NUM says how how many functions outside the current one you are looking for.是: sys._getframe(NUM)其中 NUM 表示您正在寻找的当前函数之外有多少函数。 The returned frame object has attributes like f_lineno and f_code.co_filename .返回的框架对象具有f_linenof_code.co_filename等属性。

http://docs.python.org/library/sys.html#sys._getframe http://docs.python.org/library/sys.html#sys._getframe

Try stacklevel , which counts the number of calls from the original logging call to the logger's debug() , info() , etc., call.尝试stacklevel ,它计算从原始日志记录调用到记录器的debug()info()等调用的调用次数。 It's new in logging 3.8:这是日志记录3.8 中的新功能:

The third optional keyword argument is stacklevel, which defaults to 1. If greater than 1, the corresponding number of stack frames are skipped when computing the line number and function name set in the LogRecord created for the logging event.第三个可选关键字参数是 stacklevel,默认为 1。如果大于 1,则在计算为日志记录事件创建的 LogRecord 中设置的行号和函数名时,将跳过相应数量的堆栈帧。 This can be used in logging helpers so that the function name, filename and line number recorded are not the information for the helper function/method, but rather its caller.这可以用于记录帮助器,以便记录的函数名、文件名和行号不是帮助器函数/方法的信息,而是它的调用者的信息。 The name of this parameter mirrors the equivalent one in the warnings module.此参数的名称反映了 warnings 模块中的等效名称。

Here's yet another stab at rewriting findCaller .这是重写findCaller的又一次尝试。 This lets you customize the extra stack frame depth on a per-function basis.这使您可以在每个函数的基础上自定义额外的堆栈帧深度。

import os
import logging
from contextlib import contextmanager

logging.basicConfig(
    format='%(asctime)-15s  %(levelname)s  %(filename)s:%(lineno)d  %(message)s',
    level=logging.INFO
)


@contextmanager
def logdelta(n, level=logging.DEBUG):
    _frame_stuff = [0, logging.Logger.findCaller]

    def findCaller(_):
        f = logging.currentframe()
        for _ in range(2 + _frame_stuff[0]):
            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 == logging._srcfile:
                f = f.f_back
                continue
            rv = (co.co_filename, f.f_lineno, co.co_name)
            break
        return rv

    rootLogger = logging.getLogger()
    isEnabled = rootLogger.isEnabledFor(level)
    d = _frame_stuff[0]
    try:
        logging.Logger.findCaller = findCaller
        _frame_stuff[0] = d + n
        yield isEnabled
    except:
        raise
    finally:
        logging.Logger.findCaller = _frame_stuff[1]
        _frame_stuff[0] = d


def A(x):
    with logdelta(1):
        logging.info('A: ' + x)    # Don't log with this line number

def B(x):
    with logdelta(2):
        logging.info('A: ' + x)    # or with this line number

def C(x):
    B(x)                       # or this line number

A('hello')                     # Instead, log with THIS line number
C('hello')                     # or THIS line number```

The best place to change lineno and filename (if you really want to do that) is in a filter that is attached to the logger.更改 lineno 和文件名的最佳位置(如果你真的想这样做)是在附加到记录器的过滤器中。 Here is a proof-of-concept.这是一个概念验证。 The last line will log a customized message with filename and line of the last line:最后一行将记录带有文件名和最后一行行的自定义消息:

import logging
import inspect
from pathlib import Path

class up_stacked_logger:
    def __init__(self, logger, n):
        self.logger = logger

        calling_frame = inspect.stack()[n+1].frame
        trace = inspect.getframeinfo(calling_frame)  

        class UpStackFilter(logging.Filter):
            def filter(self, record):       
                record.lineno = trace.lineno
                record.pathname = trace.filename
                record.filename = Path(trace.filename).name
                return True

        self.f = UpStackFilter()

    def __enter__(self):
        self.logger.addFilter(self.f)
        return self.logger

    def __exit__(self, *args, **kwds):
        self.logger.removeFilter(self.f)


def my_cool_customized_log_function(logger, msg):  

    with up_stacked_logger(logger, n=1) as logger:
        logger.info(f'--> customize {msg}')


logging.basicConfig(level=logging.DEBUG, format="[%(name)s][%(levelname)s]  %(message)s (%(filename)s:%(lineno)d)")
logger = logging.getLogger('some.module.logger')  

my_cool_customized_log_function(logger, 'a message')

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM