Welcome, guest | Sign In | My Account | Store | Cart
  • Just import, call and log your selected call/return/exception/etc. events.

  • You can use a standard Python logger from the logging module.

  • Quite flexible tool and about 50 efective SLOC only (excluding the example script).

  • For debugging rather than production environments (programs noticeably slow down).

Python, 124 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python
# Copyright (c) 2010-2011 Jan Kaliszewski (zuo). All rights reserved.
# Licensed under the MIT License. Python 2.6/2.7-compatibile.

import os.path
import sys
from contextlib import contextmanager
from repr import repr as default_reprfunc

__all__ = 'trace_logging_on',

default_refdir = os.path.dirname(sys.modules['__main__'].__file__)


def trace_logging_on(refdir=default_refdir,    # reference-point directory path
                     negprefix=('..', '<'),  # negative filtering path prefixes
                     filterfunc=(lambda s: True),   # custom filtering function
                     filterarg='',     # .format()-able filter argument pattern
                                       # e.g. '{event}&{path}&{frame.f_lineno}'
                     events2log = { # mapping events to .format()-able log patt
                         'call': '[C  ] {path}: {name}{callargs}',
                         'return': '[  R] {path}: {name}  ->  {argrepr}',
                         'exception': '[ E ] {path}, in {name}:\n{traceback}',
                     },  # [.format()-able patterns refer to tracer()'s locals]
                     logger='',                       # logger name or instance
                     loggermethod='debug',                 # logger method name
                     reprfunc=default_reprfunc):  # repr()-replacement function

    """
    Enable logging of Python call/return/exception events (handily filtered).

    Usage variants:
    * simply-turn-on call:        trace_logging_on(...)
    * context manager syntax:     with trace_logging_on(...): ...
    * context manager with 'as':  with trace_logging_on(...) as tracefunc: ...
    """

    from inspect import getargvalues, formatargvalues
    from traceback import format_exception
    if isinstance(logger, basestring):
        from logging import getLogger
        logger = getLogger(logger)
    log = getattr(logger, loggermethod)
    relpath = os.path.relpath
    formatvalue = (lambda s: '=' + reprfunc(s))
    filterarg_format = filterarg.format

    def tracer(frame, event, arg):
        if event not in events2log:  # initial event filtering
            return tracer
        path = relpath(frame.f_code.co_filename, refdir)  # relative to refdir
        # path-prefix-based scope filtering (negative, i.e. False lets by)
        if path.startswith(negprefix):
            return None  # (<- None to discontinue tracing in sub-scopes)
        # adding some locals (to be used to format filtering/logging arguments)
        name = frame.f_code.co_name
        argrepr = reprfunc(arg)
        if event == 'call':
            argvalues = (getargvalues(frame) if name != '<genexpr>'
                         else ([],) + getargvalues(frame)[1:])
            callargs = formatargvalues(*argvalues, formatvalue=formatvalue)
        elif event == 'exception':
            traceback = ''.join(format_exception(*arg))
        pattern_fields = locals()
        # callback-based individual filtering (positive, i.e. True lets by)
        if not filterfunc(filterarg_format(**pattern_fields)):
            return tracer
        # event-specific logging
        log(events2log[event].format(**pattern_fields))
        return tracer

    with_support = [_with_statement_support(tracer, previous=sys.gettrace())]
    sys.settrace(tracer)
    return with_support.pop()   # (we don't like circular references...)


@contextmanager
def _with_statement_support(tracer, previous):
    yield tracer
    sys.settrace(previous)


if __name__ == '__main__':

    ## Example script ##

    import logging
    from operator import methodcaller

    def robin(a, b, c):
        return c, b, a

    def lancelot(x, y="Let's not bicker and argue", z="about who killed who."):
        return x, y, z

    def rabbit():
        return 1 / 0

    def arthur(nee):
        lancelot('Camelot!')
        robin('Oh, shut up', 'and go', 'and change your armor!')
        robin(*lancelot("Why doesn't Lancelot go?"))
        try:
            rabbit()
        except:
            lancelot(*robin('spam', 'spam', 'spam'))
        return nee

    # enjoy your stderr :)

    logging.basicConfig(level=logging.DEBUG)
    arthur(0)  # (<- not logged)
    with trace_logging_on(logger='tim.the.enchanter',
                          reprfunc='.:{0}:.'.format):
        arthur(1)
        with trace_logging_on(refdir='/usr', negprefix=(),  # any prefix is ok
                              filterarg='{event}', filterfunc='call'.__eq__):
            arthur('2 sheds')
        arthur(3)
    arthur(4)  # (<- not logged)
    trace_logging_on(filterarg='{name}',
                     filterfunc=methodcaller('startswith', 'r'),
                     logger='comfy.chair', loggermethod='info', reprfunc=repr)
    arthur(5)

3 comments

Robin Becker 11 years, 1 month ago  # | flag

If you change the line

from repr import repr as default_reprfunc

to

try:
    from repr import repr as default_reprfunc
except:
    default_reprfunc = repr

try:
    basestring
except NameError:
    basestring = (str,bytes)

then the code works in python 3.x

Jan Kaliszewski (author) 11 years, 1 month ago  # | flag

Thanx Robin for your comment.

I'd rather write:

try:
    from repr import repr as default_reprfunc
except:
    from reprlib import repr as default_reprfunc

try:
    basestring
except NameError:
    basestring = str

To problem is that with Py3k (3.1) I got also some other errors -- related to Python 3 exception chaining (message mentioned __cause__ attribute etc.) -- and I haven't had time to investigate what's wrong (probably I will when I have some spare time).

Jan Kaliszewski (author) 10 years, 7 months ago  # | flag

2011-04-29: GenExp-related problem fixed.