4

When I want to log some specific Exception, but otherwise ignore it, I can do that like so:

import logging

logger = logging.getLogger(__name__)

try:
    something_that_may_fail()
except NameError:
    logger.error("It failed with:", exc_info=True)

(This is in fact an MRE, as something_that_may_fail hasn't been defined, so the try block will raise NameError with message name 'something_that_may_fail' is not defined. )

This however will also log the stack trace:

It failed with:
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: name 'something_that_may_fail' is not defined

Sometimes that isn't what I want: In some cases, I already know that exception type and exception message (together with my custom log message) will suffice, and don't want to expand the log with stack traces that don't tell me anything new. So I'd want a log entry that simply is

It failed with:
NameError: name 'something_that_may_fail' is not defined

I can achieve that by passing a 3-tuple as the exc_info, with the stack trace component replaced by None:

import logging
import sys

logger = logging.getLogger(__name__)

try:
    something_that_may_fail()
except NameError:
    exc_type, exc_value, _trace = sys.exc_info()
    logger.error("It failed with:", exc_info=(exc_type, exc_value, None))

But I'm not sure how reliable that is. (The documentation doesn't mention how the tuple may or may not deviate from one returned by sys.exc_info().)

Examining the exception myself with

...
except NameError as e:
    ...

comes with its own problems:

  • f"{type(e)}" gives string <class 'NameError'> instead of just string NameError
  • Can I rely on the message always being e.args[0]? (I might have uses for other exceptions (with more sub-types) than just NameError, which I've used here only as an example.)

So what is the proper way to log exception type and message without the stack trace? Is there a cleaner way than my make-the-trace-None hack above?

das-g
  • 9,718
  • 4
  • 38
  • 80
  • I may not fully understand the question, but how about this: `logger.error(f"It failed with: {e.__class__.__name__}: {e}")` – tituszban Jul 26 '21 at 15:51
  • 1
    The internal structure of an exception is not standardized. You can't really assume much at all about an arbitrary exception, though if you are catching a specific *kind* of exception, you can make assumptions about exceptions of that specific type. – chepner Jul 26 '21 at 15:52
  • 1
    @tituszban `e.__class__.__name__` won't tell me the module the Exception is from. (Which might be important, e.g. if it comes from a third-party library.) – das-g Jul 26 '21 at 15:54
  • @chepner, I'm looking for a solution that will also work when I won't know all types of exceptions I might handle there, e.g. in an `except Exception:` or even `except:` block. (Yes, I know one usually shouldn't use those, especially not the latter.) – das-g Jul 26 '21 at 15:57
  • Like I said, exception structure isn't standardized. You *can't* handle an arbitrary exception without first seeing what type it is, then branching to specific code for that exception type when necessary. – chepner Jul 26 '21 at 16:03
  • Does that mean that there be some exceptions that `logger.error(..., exc_info=True)` or ``logger.error(..., exc_info=sys.exc_info())`` can't properly handle, either? @chepner – das-g Jul 26 '21 at 16:09
  • 1
    Instead of `e.__class__.__name__` you could use [`e.__class__.__qualname__`](https://docs.python.org/3/library/stdtypes.html#definition.__qualname__), which contains the module as well. – Sven Eberth Jul 26 '21 at 16:09
  • @SvenEberth, That doesn't seem to be the case. Try `import requests; print(requests.Timeout.__qualname__)`. Output on Python 3.7.10 is only `Timeout`. Or with only Python standard library: `import json; print(json.JSONDecodeError.__qualname__)` which gives just `JSONDecodeError`. – das-g Jul 26 '21 at 16:25
  • (`__qualname__` is still useful for nested classes, see https://stackoverflow.com/a/2020083/674064) – das-g Jul 26 '21 at 16:26
  • @das-g Oh, I should have had a look at the according [PEP](https://www.python.org/dev/peps/pep-3155/#excluding-the-module-name) before... My mistake, sorry. Then you must also use `__module__` (like in the answer your linked). – Sven Eberth Jul 26 '21 at 16:47

1 Answers1

4

traceback.format_exception_only can be used for that:

import logging
import sys
import traceback

logger = logging.getLogger(__name__)

try:
    something_that_may_fail()
except NameError:
    exc_type, exc_value, _trace = sys.exc_info()
    exc_desc_lines = traceback.format_exception_only(exc_type, exc_value)
    exc_desc = ''.join(exc_desc_lines).rstrip()
    logger.error(f"It failed with:\n{exc_desc}")

or without sys:

import logging
import traceback

logger = logging.getLogger(__name__)

try:
    something_that_may_fail()
except NameError as e:
    exc_desc_lines = traceback.format_exception_only(type(e), e)
    exc_desc = ''.join(exc_desc_lines).rstrip()
    logger.error(f"It failed with:\n{exc_desc}")

(Found this by looking how the logging module actually extracts and formats information from exc_info. There traceback.print_exception is being used, so I looked what else is available in the traceback module.)

das-g
  • 9,718
  • 4
  • 38
  • 80