omercnet
omercnet

Reputation: 737

Custom exception default logging

I've built custom exceptions that accept a parameter(s) and format their own message from constants. They also print to stdout so the user understands the issue.

For instance:

defs.py:
PATH_NOT_FOUND_ERROR = 'Cannot find path "{}"'

exceptions.py:
class PathNotFound(BaseCustomException):
    """Specified path was not found."""

    def __init__(self, path):
        msg = PATH_NOT_FOUND_ERROR.format(path)
        print(msg)
        super(PathNotFound, self).__init__(msg)

some_module.py
raise PathNotFound(some_invalid_path)

I also want to log the exceptions as they are thrown, the simplest way would be:

logger.debug('path {} not found'.format(some_invalid_path)
raise PathNotFound(some_invalid_path)

But doing this all across the code seems redundant, and especially it makes the constants pointless because if I decide the change the wording I need to change the logger wording too.

I've trying to do something like moving the logger to the exception class but makes me lose the relevant LogRecord properties like name, module, filename, lineno, etc. This approach also loses exc_info

Is there a way to log the exception and keeping the metadata without logging before raising every time?

Upvotes: 3

Views: 992

Answers (1)

omercnet
omercnet

Reputation: 737

If anyone's interested, here's a working solution

The idea was to find the raiser's frame and extract the relevant information from there. Also had to override logging.makeRecord to let me override internal LogRecord attributes

Set up logging

class MyLogger(logging.Logger):
    """Custom Logger."""

    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None):
        """Override default logger to allow overridding internal attributes."""
        if six.PY2:
            rv = logging.LogRecord(name, level, fn, lno, msg, args, exc_info, func)
        else:
            rv = logging.LogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo)

        if extra is not None:
            for key in extra:
                # if (key in ["message", "asctime"]) or (key in rv.__dict__):
                #     raise KeyError("Attempt to overwrite %r in LogRecord" % key)
                rv.__dict__[key] = extra[key]
        return rv



logging.setLoggerClass(MyLogger)
logger = logging.getLogger(__name__)

Custom Exception Handler

class BaseCustomException(Exception):
    """Specified path was not found."""

    def __init__(self, path):
    """Override message with defined const."""
    try:
        raise ZeroDivisionError
    except ZeroDivisionError:
        # Find the traceback frame that raised this exception
        exception_frame = sys.exc_info()[2].tb_frame.f_back.f_back

    exception_stack = traceback.extract_stack(exception_frame, limit=1)[0]
    filename, lineno, funcName, tb_msg = exception_stack

    extra = {'filename': os.path.basename(filename), 'lineno': lineno, 'funcName': funcName}
    logger.debug(msg, extra=extra)
    traceback.print_stack(exception_frame)
    super(BaseCustomException, self).__init__(msg)

Upvotes: 2

Related Questions