3

There are several logging.warn('....') calls in the legacy code base I am working on today.

I want to understand the log output better. Up to now logging.warn() does emit one line. But this single line is not enough to understand the context.

I would like to see the stacktrace of the interpreter.

Since there are a lot of logging.warn('....') lines in my code, I would like to leave them like they are and only modify the configuration of the logging.

How can I add the interpreter stacktrace to every warn() or error() call automatically?

I know that logging.exception("message") shows the stacktrace, but I would like to leave the logging.warn() lines untouched.

codeforester
  • 39,467
  • 16
  • 112
  • 140
guettli
  • 25,042
  • 81
  • 346
  • 663
  • The way I've handled this is using `logging.exception("message")` in an `except` clause. – tda Sep 04 '18 at 08:59
  • @TD-Asker I updated the answer: I know that logging.exception("message") shows the stacktrace, but I would like to leave the logging.warn() lines untouched. – guettli Sep 04 '18 at 09:08
  • 1
    I think you may struggle then, since `.warn()` will only return the contained string and does not have knowledge of the stack trace. Changing the logging configuration will not alter the knowledge that `.warn()` contains. Someone may know a hack/workaround though so good luck! – tda Sep 04 '18 at 09:14
  • there isn't suck config option in logging module to my knowledge, I may write a custom handler to achieve this. – georgexsh Sep 04 '18 at 09:23
  • create a custom class of inheriting logging class and then override the warn function and initialize the `logging = new loggingclass` this way you don't need to change anything and when work is done remove the `logging = new loggingclass` – Ankur Jyoti Phukan Sep 07 '18 at 08:19
  • check that out: https://stackoverflow.com/questions/45562594/stacktrace-for-userwarning maybe you may forge a function something like the answer of that question. – Ozgur Bagci Sep 07 '18 at 08:29
  • 1
    @OzgurBagci: `warnings` != `logging`. While you could monkey-patch both, there are better options for either framework. – Martijn Pieters Sep 09 '18 at 13:26
  • @AnkurJyotiPhukan: that's.. rather overkill when the logging library has so many hooks to add info to log records. – Martijn Pieters Sep 09 '18 at 13:26
  • 3
    What Python release should this work on? Python 3.x has the `stack_info` flag, which you could trivially force. – Martijn Pieters Sep 09 '18 at 13:28
  • @MartijnPieters I am still using Python2.7. – guettli Sep 09 '18 at 19:15

3 Answers3

5

The answer I was looking for was given by @Martijn Pieters♦ in the comments

In python 3.x

logger.warning(f'{error_message}', stack_info=True)

does exactly what you need.

Thanks @Martijn Pieters♦

Prakash
  • 471
  • 1
  • 8
  • 19
2

it is trivial if you accept to add a log handler:

import logging
import traceback


class WarnWithStackHandler(logging.StreamHandler):
    def emit(self, record):
        if record.levelno == logging.WARNING:
            stack = traceback.extract_stack()
            # skip logging internal stacks
            stack = stack[:-7]
            for line in traceback.format_list(stack):
                print(line, end='')
        super().emit(record)
georgexsh
  • 15,984
  • 2
  • 37
  • 62
1

I don't believe a handler is your solution. Go for a filter:

import os.path
import traceback
import logging
_LOGGING_FILE = os.path.normcase(logging.addLevelName.__code__.co_filename)
_CURRENT_FILE = os.path.normcase(__file__)
_ELIMINATE_STACK = (_CURRENT_FILE, _LOGGING_FILE)


class AddStackFilter(logging.Filter):
    def __init__(self, levels=None):
        self.levels = levels or set()

    def get_stack(self):
        # Iterator over file names
        filenames = iter(_ELIMINATE_STACK)
        filename = next(filenames, "")
        frames = traceback.walk_stack(None)

        # Walk up the frames
        for frame, lineno in frames:

            # If frame is not from file, continue on to the next file
            while os.path.normcase(frame.f_code.co_filename) != filename:
                filename = next(filenames, None)
                if filename is None:
                    break
            else:
                # It's from the given file, go up a frame
                continue

            # Finished iterating over all files
            break

        # No frames left
        else:
            return None

        info = traceback.format_stack(frame)
        info.insert(0, 'Stack (most recent call last):\n')

        # Remove last newline
        info[-1] = info[-1].rstrip()

        return "".join(info)

    def filter(self, record):
        if record.levelno in self.levels:
            sinfo = self.get_stack()
            if sinfo is not None:
                record.stack_info = sinfo

        return True

This filter has numerous advantages:

  1. Removes stack frames from the local file and logging's file.
  2. Leaves stack frames in case we come back to the local file after passing through logging. Important if we wish to use the same module for other stuff.
  3. You can attach it to any handler or logger, doesn't bind you to StreamHandler or any other handler.
  4. You can affect multiple handlers using the same filter, or a single handler, your choice.
  5. The levels are given as an __init__ variable, allowing you to add more levels as needed.
  6. Allows you to add the stack trace to the log, and not just print.
  7. Plays well with the logging module, putting the stack in the correct place, nothing unexpected.

Usage:

>>> import stackfilter
>>> import logging
>>> sfilter = stackfilter.AddStackFilter(levels={logging.WARNING})
>>> logging.basicConfig()
>>> logging.getLogger().addFilter(sfilter)
>>> def testy():
...     logging.warning("asdasd")
...
>>> testy()
WARNING:root:asdasd
Stack (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in testy
Bharel
  • 23,672
  • 5
  • 40
  • 80
  • one issue is in python2, `logging.Formatter` doesn't format `record.stack_info` out of box, maybe you could piggyback it into `record.exc_info`. – georgexsh Sep 11 '18 at 05:56