1

I want a context manager to catch an exception, print the stack trace, and then allow execution to continue.

I want to know if I can do this with the contextlib contextmanager decorator. If not, how can I do it?

Documentation suggests the following:

At the point where the generator yields, the block nested in the with statement is executed. The generator is then resumed after the block is exited. If an unhandled exception occurs in the block, it is reraised inside the generator at the point where the yield occurred. Thus, you can use a try…except…finally statement to trap the error (if any), or ensure that some cleanup takes place. If an exception is trapped merely in order to log it or to perform some action (rather than to suppress it entirely), the generator must reraise that exception.

So I try the obvious approach that the documentation leads me to:

import contextlib
import logging


@contextlib.contextmanager
def log_error():
    try:
        yield
    except Exception as e:
        logging.exception('hit exception')
    finally:
        print 'done with contextmanager'


def something_inside_django_app():
    with log_error():
        raise Exception('alan!')


something_inside_django_app()


print 'next block of code'

This produces the output

ERROR:root:hit exception
Traceback (most recent call last):
  File "exception_test.py", line 8, in log_error
    yield
  File "exception_test.py", line 17, in something_inside_django_app
    raise Exception('alan!')
Exception: alan!
done with contextmanager
next block of code

This loses critical information about where the exception was raised from. Consider what you get when you adjust the context manager to not supress the exception:

Traceback (most recent call last):
  File "exception_test.py", line 20, in <module>
    something_inside_django_app()
  File "exception_test.py", line 17, in something_inside_django_app
    raise Exception('alan!')
Exception: alan!

Yes, it was able to tell me that the exception was raised from line 17, thank you very much, but the prior call at line 20 is lost information. How can I have the context manager give me the actual full call stack and not its truncated version of it? To recap, I want to fulfill two requirements:

  • have a python context manager suppress an exception raised in the code it wraps
  • print the stack trace that would have been generated by that code, had I not been using the context manager

If this cannot be done with the decorator, then I'll use the other style of context manager instead. If this cannot be done with context managers, period, I would like to know what a good pythonic alternative is.

AlanSE
  • 2,597
  • 2
  • 29
  • 22
  • similar to https://stackoverflow.com/questions/3702675/how-to-print-the-full-traceback-without-halting-the-program – AlanSE May 08 '18 at 13:15

1 Answers1

1

I have updated my solution for this problem here:

https://gist.github.com/AlanCoding/288ee96b60e24c1f2cca47326e2c0af1

There was more context that the question missed. In order to obtain the full stack at the point of exception, we need both the traceback returned to the context manager, and the current context. Then we can glue together the top of the stack with the bottom of the stack.

To illustrate the use case better, consider this:

def err_method1():
    print [1, 2][4]


def err_method2():
    err_method1()


def outside_method1():
    with log_error():
        err_method2()


def outside_method2():
    outside_method1()

outside_method2()

To really accomplish what this question is looking for, we want to see both outer methods, and both inner methods in the call stack.

Here is a solution that does appear to work for this:

class log_error(object):

    def __enter__(self):
        return

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if exc_value:
            # We want the _full_ traceback with the context, so first we
            # get context for the current stack, and delete the last 2
            # layers of context, saying that we're in the __exit__ method...
            top_stack = StringIO.StringIO()
            tb.print_stack(file=top_stack)
            top_lines = top_stack.getvalue().strip('\n').split('\n')[:-4]
            top_stack.close()
            # Now, we glue that stack to the stack from the local error
            # that happened within the context manager
            full_stack = StringIO.StringIO()
            full_stack.write('Traceback (most recent call last):\n')
            full_stack.write('\n'.join(top_lines))
            full_stack.write('\n')
            tb.print_tb(exc_traceback, file=full_stack)
            full_stack.write('{}: {}'.format(exc_type.__name__, str(exc_value)))
            sinfo = full_stack.getvalue()
            full_stack.close()
            # Log the combined stack
            logging.error('Log message\n{}'.format(sinfo))
        return True

The traceback looks like:

ERROR:root:Log message
Traceback (most recent call last):
  File "exception_test.py", line 71, in <module>
    outside_method2()
  File "exception_test.py", line 69, in outside_method2
    outside_method1()
  File "exception_test.py", line 65, in outside_method1
    err_method2()
  File "exception_test.py", line 60, in err_method2
    err_method1()
  File "exception_test.py", line 56, in err_method1
    print [1, 2][4]
IndexError: list index out of range

This is the same information that you would expect from doing logging.exception in a try-except over the same code that you wrap in the context manager.

AlanSE
  • 2,597
  • 2
  • 29
  • 22