Reputation: 2549
When writing defensive code in python (e.g. you're handling some user input or whatever), I find it useful to return Exception
objects alongside regular computation results, so they can be discarded/logged or processed in some other way. Consider the following snippet:
import logging
from traceback import TracebackException
from typing import Union
logging.basicConfig(level=logging.INFO)
def _compute(x) -> int:
return len(x)
def compute(x) -> Union[int, Exception]:
try:
return _compute(x)
except Exception as e:
return e
inputs = [
'whatever',
1,
'ooo',
None,
]
outputs = []
for i in inputs:
r = compute(i)
outputs.append(r)
for i, r in zip(inputs, outputs):
logging.info('compute(%s)', i)
if isinstance(r, Exception):
logging.exception(r)
else:
logging.info(r)
This results in the following output
INFO:root:compute(whatever)
INFO:root:8
INFO:root:compute(1)
ERROR:root:object of type 'int' has no len()
NoneType: None
INFO:root:compute(ooo)
INFO:root:3
INFO:root:compute(None)
ERROR:root:object of type 'NoneType' has no len()
NoneType: None
So you can see that useful exception information like stacktrace is lost, which makes it a bit hard to debug the cause of exception.
This can be fixed by logging exception as logging.exception(r, exc_info=r)
:
INFO:root:compute(whatever)
INFO:root:8
INFO:root:compute(1)
ERROR:root:object of type 'int' has no len()
Traceback (most recent call last):
File "/tmp/test.py", line 15, in compute
return _compute(x)
File "/tmp/test.py", line 10, in _compute
return len(x)
TypeError: object of type 'int' has no len()
INFO:root:compute(ooo)
INFO:root:3
INFO:root:compute(None)
ERROR:root:object of type 'NoneType' has no len()
Traceback (most recent call last):
File "/tmp/test.py", line 15, in compute
return _compute(x)
File "/tmp/test.py", line 10, in _compute
return len(x)
TypeError: object of type 'NoneType' has no len()
My question is -- why doesn't logging.exception
method do this by default, if the argument passed to it happens to be an Exception
? I tried searching in PEPs/etc but wasn't really fruitful.
My only guess is that logging.exception
is essentially just a special case of logging.error
, so in principle logging.exception
method doesn't know whether is' passed an Exception
object or something else. So supporting this would require some code, e.g. checking whether isinstance(msg, Exception)
, and perhaps the authors of logging library decided it's a bit too specific. But IMO it makes sense considering in practice in most cases logging.exception
is passed an Exception
object.
Upvotes: 1
Views: 756
Reputation: 2549
While it still doesn't answer the question why, I came up with a workaround:
You can define a Filter
object that automatically adds exc_info
to any exceptions, regardless whether they are called from the exception handler or not.
class AddExceptionTraceback(logging.Filter):
def filter(self, record):
s = super().filter(record)
if s is False:
return False
if record.levelname == 'ERROR':
exc = record.msg
if isinstance(exc, BaseException):
if record.exc_info is None or record.exc_info == (None, None, None):
exc_info = (type(exc), exc, exc.__traceback__)
record.exc_info = exc_info
return s
logger.addFilter(AddExceptionTraceback())
I used it for a while now without any issues whatsoever, so maybe it'd help someone else.
Upvotes: 0
Reputation: 281604
logging.exception
does log the traceback by default. However, you're using it wrong. As the docs say,
This function should only be called from an exception handler.
logging.exception
does not expect to be passed an exception instance, or any sort of exception information whatsoever. It uses sys.exc_info
to gather info about the exception currently being handled, which only works if an exception is currently being handled. If you call it outside of an exception handler, it breaks.
Upvotes: 2