powertools-lambda-python icon indicating copy to clipboard operation
powertools-lambda-python copied to clipboard

Support `@classmethod` in `Tracer.capture_method()`

Open tibbe opened this issue 2 years ago • 2 comments

Use case

Sometimes you want to trace a @classmethod. In my case I have an ORM-like (https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) library that provides some get, put, query, etc. class methods in a base class. I want tracing for all subclasses. Something like:

class Table:
    @classmethod
    @tracer.capture_method  # Unclear which should nest innermost.
    def get(cls, id: str):
        db.get(table_name=cls.table_name, id=id)

    table_name: str

class Users:
    table_name = "users"

user = Users.get(123)

This doesn't work at the moment (fails with an error about get not having __module__ defined IIRC, at least if you nest @classmethod innermost).

Solution/User Experience

Either extend capture_method to cover @classmethods or add another decorator.

Ideally, as far as I'm concerned, we actually want to use the actual class (cls) in the sub-segment name. So in the example above "## user_module.Users.get". This provides more information in the span name than using Table. You could extend the same argument to the existing behavior of capture_method for instance methods. Although changing it there would be a breaking change (although probably benign).

Alternative solutions

Currently I've written my own, limited workaround:

_P = ParamSpec('_P')
_R_co = TypeVar('_R_co', covariant=True)


_Class = TypeVar('_Class', bound=type)

# Tracer.capture_method does not work for classmethods, so we need our own
# version.
def decorate_sync_classmethod(
    func: Callable[Concatenate[_Class, _P], _R_co],
) -> Callable[Concatenate[_Class, _P], _R_co]:
    '''Equivalent to `@Tracer.capture_method`, but for classmethods.

    Only works on sync (i.e. not async) classmethods.
    '''
    @functools.wraps(func)
    def wrapper(cls: _Class, *args: _P.args, **kwargs: _P.kwargs) -> _R_co:
        method_name = f'{cls.__module__}.{cls.__qualname__}.{func.__name__}'
        with _tracer.provider.in_subsegment(name=f"## {method_name}"):
            return func(cls, *args, **kwargs)
    return wrapper

Acknowledgment

tibbe avatar Mar 15 '23 15:03 tibbe

We're on Python 3.9. Seems like starting with 3.10 __module__ is defined: https://docs.python.org/3/library/functions.html#classmethod. For older Python versions we might have to work around it by digging out the module from cls directly.

tibbe avatar Mar 15 '23 20:03 tibbe

Hey Johan, thanks a lot for creating the feature request. I did a quick research and it turns out it’s more complex than I imagined it to be.

A classmethod returns a descriptor which is devoid from call. Therefore, we’d need to create a new decorator that is actually a class that decorates bounded methods that call unbounded function objects for it to work safely.

For now, we should update the Tracer documentation with a FAQ and/or new section that customers would need to swap the order @.*** being the top-most).

After that, I’d love to hear feedback from you on what a new decorator name would look like. I thought we could extend capture_method easily but the more I think about it the less ideal it is — e.g., what if the static method is async?

Suffice to say we welcome contributions and experiments in this area until we can prioritise it accordingly.

Thanks a lot for kicking this off; TIL.

https://github.com/GrahamDumpleton/wrapt/blob/master/blog/02-the-interaction-between-decorators-and-descriptors.md

On Wed, 15 Mar 2023 at 21:25, Johan Tibell @.***> wrote:

We're on Python 3.9. Seems like starting with 3.10 module is defined: https://docs.python.org/3/library/functions.html#classmethod. For older Python versions we might have to work around it by digging out the module from cls directly.

— Reply to this email directly, view it on GitHub https://github.com/awslabs/aws-lambda-powertools-python/issues/2011#issuecomment-1470791908, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAZPQBGYMI23Z4MIOO6X2ALW4IQVDANCNFSM6AAAAAAV37NQ3A . You are receiving this because you are subscribed to this thread.Message ID: @.*** com>

heitorlessa avatar Mar 16 '23 07:03 heitorlessa