Skip to content

Instantly share code, notes, and snippets.

@ScheerMT
Created June 4, 2019 12:40
Show Gist options
  • Select an option

  • Save ScheerMT/8a4016af2d5206edc16a17a525e8ac7d to your computer and use it in GitHub Desktop.

Select an option

Save ScheerMT/8a4016af2d5206edc16a17a525e8ac7d to your computer and use it in GitHub Desktop.

Revisions

  1. ScheerMT created this gist Jun 4, 2019.
    330 changes: 330 additions & 0 deletions log_decorator.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,330 @@
    from functools import wraps, partial
    import logging
    import six

    # Testing function decorator version
    def log_func():
    return log_func_internal

    def log_func_internal(func):
    @wraps(func)
    def wrapper(*args, **kwargs):

    args_repr = [repr(a) for a in args]

    #kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
    kwargs_repr = ["{0}={1}".format(k,v) for k, v in kwargs.items()]

    kwargs_repr_dict = {k: v for k, v in kwargs.items()}


    signature = ", ".join(args_repr + kwargs_repr)

    print(kwargs_repr_dict)
    print("Calling {0}({1})".format(func.__name__, signature))

    print(func.__module__)

    returned_value = func(*args, **kwargs)

    print("{0} returned {1}".format(func.__name__, returned_value))

    return returned_value

    return wrapper



    # TODO: maybe implement a function to allow better optional handling (right now default decorator use forces a set of closing parenthesis?
    # https://chase-seibert.github.io/blog/2013/12/17/python-decorator-optional-parameter.html#

    # TODO: Rename class lol
    class LogMe(object):
    def __init__(self, *args, **kwargs):
    self.__wfunc = None
    self.service_name = kwargs.pop('service_name', None)
    self.logger = logging.getLogger('default')
    self.other_args = kwargs

    @property
    def service_name(self):
    """Each logger has should be associated with a service for ease of tracking in logs.
    If no name is defined, we fallback onto a default named 'SERVICE_DEFAULT_NAME'
    """
    return self._service_name

    @service_name.setter
    def service_name(self, value):
    DEFAULT_SERVICE_NAME = 'SERVICE_DEFAULT_NAME'
    self._service_name = value if value else DEFAULT_SERVICE_NAME

    @property
    def __wfunc(self):
    """The wrapped function we will be working with"""
    return self.___wfunc if self.___wfunc else None

    @__wfunc.setter
    def __wfunc(self, value):
    self.___wfunc = value

    @property
    def __function_name(self):
    """The __name__ of the wrapped function we are working with.
    We fallback to an empoty string if we do not have a function
    """
    return self.__wfunc.__name__ if self.__wfunc else ''

    def debug(self, msg, **kwargs):
    """Log a debug level message with any extra keyword parameters
    Arguments:
    msg {string} -- A string that will be logged out
    Keyword Arguments:
    These will be appended to the logged message as key value pairs output to a string representation
    """
    self.log(level=logging.DEBUG, msg=msg, **kwargs)

    def info(self, msg, **kwargs):
    """Log an info level message with any extra keyword parameters
    Arguments:
    msg {string} -- A string that will be logged out
    Keyword Arguments:
    These will be appended to the logged message as key value pairs output to a string representation
    """
    self.log(level=logging.INFO, msg=msg, **kwargs)


    def warn(self, msg, **kwargs):
    """Log a warning level message with any extra keyword parameters
    Arguments:
    msg {string} -- A string that will be logged out
    Keyword Arguments:
    These will be appended to the logged message as key value pairs output to a string representation
    """
    self.log(level=logging.WARNING, msg=msg, **kwargs)


    def error(self, msg, **kwargs):
    """Log an error level message with any extra keyword parameters
    Arguments:
    msg {string} -- A string that will be logged out
    Keyword Arguments:
    These will be appended to the logged message as key value pairs output to a string representation
    """
    self.log(level=logging.ERROR, msg=msg, **kwargs)


    def exception(self, msg, **kwargs):
    """Log an error level message with any extra keyword parameters
    Arguments:
    msg {string} -- A string that will be logged out
    Keyword Arguments:
    These will be appended to the logged message as key value pairs output to a string representation
    """
    self.error(msg=msg, exc_info=True, **kwargs)


    def log(self, msg, **kwargs):
    """Log a message, at a specified level
    Arguments:
    msg {string} -- A string that will be logged out
    Keyword Arguments:
    level {Logging.DEBUG/INFO/etc} -- one of the defined logging levels
    """

    # Determine level with a sane default
    # TODO: get defensive about log levels passed in here - what should be allowed?
    level = kwargs.pop('level', logging.DEBUG)

    # Passing these through to properly allow stack traces/info
    exc_info = kwargs.pop('exc_info', False)

    # This is a py3 only option!
    stack_info = kwargs.pop('stack_info', False)

    extra = kwargs if kwargs else {}

    if extra != {}:
    extra.update(self.other_args)
    else:
    # If we don't have any arguments passed in, we need to still attach the other args passed in on init
    extra = self.other_args if self.other_args else ''

    # Keep it defensive - ensure we do not fail logging on string conversion
    try:
    extra_str = str(extra)
    except:
    extra_str = {}


    if six.PY2:
    # Log that message!
    self.logger.log(
    level=level,
    exc_info=exc_info,
    extra=extra,
    msg= self.service_name + '::' + self.__function_name + '::' + msg + ':::' + extra_str
    )
    else:
    # Log that message!
    self.logger.log(
    level=level,
    exc_info=exc_info,
    stack_info=stack_info,
    extra=extra,
    msg= self.service_name + '::' + self.__function_name + '::' + msg + ':::' + extra_str
    )

    # Callable! this is what helps us turn this class into a decorator
    # an attempt is made to ensure, if attempted to be used as a callable class outside of a function call, we have a fallback
    def __call__(self, func=None):
    # Ensure we update our stored function that we are working with wrapping
    # This is needed for logging output of function name etc
    self.__wfunc = func

    """ Determine how best to use this?"""
    def wrapped(*args, **kwargs):
    self.log('bare call' + str(kwargs))

    # This needs to appear first because the @wraps call bombsa out if you are not wrapping via a decorator call
    if func is None:
    return wrapped

    # Decorator magic~
    @wraps(func)
    def wrapped_func(*args, **kwargs):
    args_repr = [repr(a) for a in args]
    kwargs_repr = ["{0}={1}".format(k,v) for k, v in kwargs.items()]
    signature = ", ".join(args_repr + kwargs_repr)

    # Pre function call logging
    self.log(
    'Calling the function with the following',
    func_signature="{0}({1})".format(self.__function_name, signature),
    args_list=", ".join(args_repr),
    kwargs_list=", ".join(kwargs_repr),
    kwargs_dict=kwargs
    )

    # Ensure we run the wrapped func and capture its return to be logged and returned
    returned_value = func(*args, **kwargs)

    # Keep it defensive - ensure we do not fail logging on string conversion
    try:
    returned_value_str = str(returned_value)
    except:
    returned_value_str = ''

    # Post function call logging
    self.log('WhatWasTheReturn', return_value_str=returned_value_str)

    # Returning value from function call
    return returned_value

    # Decorator magic~
    return wrapped_func



    if __name__ == '__main__':
    daServiceLogger = LogMe(service_name='daService', other='default', info=123)
    daServiceExceptionLogger = LogMe(service_name='daService', type='exception')


    @log_func()
    def do_the_thing(one, two, three='four'):
    print('hello!')
    return 1

    @daServiceLogger
    def do_the_thing_log_default(one, two, three='four'):
    print('hello!')
    return 1

    @LogMe(service_name='daService')
    def do_the_thing_log(one, two, three='four'):
    print('hello!')
    return 1


    @LogMe()
    def do_the_thing_log_bare(one, two, three='four'):
    print('hello! - bare')
    return 1

    class testOne(object):
    def __init__(self):
    self.one = 'yes'
    self.two = {'one': 'three'}
    self.three = 12345


    @LogMe()
    def do_the_thing_log_class():
    return testOne()


    ##### TESTING #####
    logging.basicConfig(level=logging.DEBUG)




    print('\n')
    # Utilizes a default logger we setup and subsequently used as a decorator
    do_the_thing_log_default(0,1,three=2)


    print('\n')
    # Uses decorator class directly and sets it up in the decorator
    do_the_thing_log(1,2,three=3)


    print('\n\n')
    # Utilizes the decorator class with defaults
    do_the_thing_log_bare(2,3,three=4)


    print('\n\n')
    # Defult class decorator setup - returning a class this time
    do_the_thing_log_class()


    print('\n\n')
    # Calling the logger directly
    LogMe(service_name='callWithDatInfo', other=1).info('heya!', one=2)

    print('\n\n')

    # testing directly...
    daServiceLogger.debug('debug me!')
    daServiceLogger.info('info me!')
    daServiceLogger.warn('warn me!')
    daServiceLogger.error('error me!')
    daServiceLogger.log('critical me!', level=logging.CRITICAL)



    try:
    raise Exception('thing')
    except Exception as e:
    # Will log at error level and, by default, include exception information
    daServiceExceptionLogger.exception('Catching this exception!')

    # This methis allows you to still log exception info while logging at a lower level
    daServiceExceptionLogger.warn('Catching this exception!', exc_info=True)


    # ????? - default class, callable then calling that? very odd but wanted to make this safe too
    LogMe()()(one=2)