Skip to content

Refactor: Tracing with a trace decorator #1620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 25, 2025
Merged

Conversation

korgan00
Copy link
Collaborator

@korgan00 korgan00 commented Apr 24, 2025

Summary

New decorator factory created. This factory makes decorators with a base class key so we can use the decorator in the functions.

this is the old style:

def xxxxx(self, request, pk=None):
    """Get a specific program in the catalog:"""
    tracer = trace.get_tracer("gateway.tracer")
    ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
    with tracer.start_as_current_span("gateway.traced_feature.function_name", context=ctx):
        [...]
# before starting the class
_trace = trace_decorator_factory("traced_feature")

[...]

# on every method that we want to trace
@_trace
def function_name(self, request, pk=None):
    """Get a specific program in the catalog:"""
    [...]

# if we need to rename it for any reason
@_trace("method_name")
def other_function_name(self, request, pk=None):
    """Get a specific program in the catalog:"""
    [...]

Deep explanation:

We need to call this lines of code before every decorated function:

tracer = trace.get_tracer("gateway.tracer")
ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
with tracer.start_as_current_span(f"gateway.{traced_feature}.{function_name}", context=ctx):

In this lines there are 3 contextual elements:

  • traced_feature: The class or feature that we are tracing and doesn't use to be the same name as the class. In the client there are also an example of 2 different values inside the same class, so we need it customizable.
  • function_name: The function name that we are tracing. It use to be the name of the function. In the client there are some examples that is not the same and maybe we want to use other name at some point, we can discuss this.
  • request(.headers): The request is the first parameter in the actions.

Knowing this, I will explain different parts.

The factory

This is a regular factory, the only pourpose of the factory is to provide a common value to all the decorators: traced_feature.

def trace_decorator_factory(traced_feature: str):
    """Factory for generate decorators for classes or features."""

    [....]

    return generated_decorator

The decorator

The common form of a decorator in python is something like:

def decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

If we want to add arguments we need another wrapping function:

def decoratorArguments(beforeMessage, afterMessage):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(beforeMessage)
            func()
            print(afterMessage)
        return wrapper
    return decorator

If we want to add OPTIONAL arguments is a bit more complicated. Python will call decoratorArguments anyway (with or without arguments), so we need to know what is the case we are covering. So:

  • if we receive a function it is calling the decorator WITHOUT arguments.
  • if we receive an string, it is calling the decorator WITH arguments.
def decoratorArguments(messageOrFunction):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if isinstance(messageOrFunction, str) # if we call it with arguments, we print.
                print(messageOrFunction)
            func()
        return wrapper

    if callable(messageOrFunction): # if we receive a function, we don't have arguments so we can pipe it.
        return decorator_trace(messageOrFunction) # we call the decorator directly

    return decorator # if we don't receive the function, we return the decorator function. Python will call it with the function.

The examples are from https://realpython.com/primer-on-python-decorators/#creating-decorators-with-optional-arguments that has a very good explanation if you need to read more about decorators.

Our decorator has an optional argument (traced_function) that is the name of the function or a custom name. As we can have a function or an string, we create an union type and we will later discriminate the cases. As you can see, the structure is the same.

def generated_decorator(traced_function: Union[FunctionType, str]):
        """
        The decorator wrapper to generate optional arguments
        if traced_function is string it will be used in the span,
        the function.__name__ attribute will be used otherwise
        """

        def decorator_trace(func: FunctionType):
            """The decorator that python call"""

            def wrapper(*args, **kwargs):
            [...]
            return wrapper

        if callable(traced_function):
            return decorator_trace(traced_function)
        return decorator_trace

The wrapper

Since we use the func.__name__ and other decorators may use it, if we return the wrapper, the func.__name__ will be wrapper and that may cause problems like django autogenerated urls. To avoid that we can use the functools.wraps decorator to copy the metadata of the wrapped function.

The args and kwargs are the args of the wrapped function, so we can extract the request from there.

@wraps(func)
def wrapper(*args, **kwargs):
    """The wrapper"""
    tracer = trace.get_tracer("gateway.tracer")
    function_name = (
        traced_function # if `traced_function` was a parameter, then we use it.
        if isinstance(traced_function, str)
        else func.__name__ # if `traced_function` was a function, we use the `func.__name__`
    ) 
    request = args[0] # extract the request
    ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
    with tracer.start_as_current_span(
        f"gateway.{traced_feature}.{function_name}", context=ctx # compose the span
    ):
        result = func(*args, **kwargs) # we call it with the args and capture the result to return it and avoid malfunction
    return result

@korgan00 korgan00 changed the title refactored tracing with trace decorator Refactor: Tracing with a trace decorator Apr 24, 2025
@korgan00 korgan00 requested a review from Tansito April 24, 2025 11:13
Copy link
Member

@Tansito Tansito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will definitely need your help here to understand what is going on 😂

@Tansito
Copy link
Member

Tansito commented Apr 25, 2025

@korgan00 can you fix the conflicts after merge #1615 ?

# Conflicts:
#	gateway/api/views/jobs.py
@korgan00 korgan00 requested a review from Tansito April 25, 2025 13:22
Copy link
Member

@Tansito Tansito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM @korgan00 , thank you so much for this awesome improvement 🙏

@Tansito Tansito merged commit f158a23 into main Apr 25, 2025
8 checks passed
@Tansito Tansito deleted the refactor/trace-decorator branch April 25, 2025 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants