django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

properly name functions

Open hiaselhans opened this issue 2 years ago • 6 comments

Is your feature request related to a problem? Please describe.

I am using django-prometheus for statistics. The view calls counters are bundled for function names and thus, all my api calls are in the name of GET /api-1.0.0:ninja.operation._sync_view I would prefer to have the original function's name or the api-name

Describe the solution you'd like

https://github.com/vitalik/django-ninja/blob/3d745e7ad7b3cf4d92644143a9e342bdbc986273/ninja/operation.py#L314-L322

we could set func.__module__ and func.__name__ here

hiaselhans avatar Apr 22 '22 06:04 hiaselhans

@hiaselhans I'm running into a similar problem when trying to get the original function name in a django middleware. Could you please tell me how you solved this issue?

elnelsonperez avatar Feb 22 '23 01:02 elnelsonperez

how did you fix this? I have the same problem with new relic.

pseidemann avatar Mar 06 '23 17:03 pseidemann

We had the same problem with New Relic, and I was able to work around it by implementing some middleware like this:

class NewRelicMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self._set_ninja_new_relic_transaction_names()

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def _set_ninja_new_relic_transaction_names(self):
        """
        Without this, all django-ninja requests/errors show up in NewRelic with a
        transaction name like `ninja.operation:PathView._sync_view`, which sucks for debugging.
        This is a gross hack to fix that, so they are actually grouped in NR correctly,
        by the actual api endpoint function name
        """

        def nr_wrapper(api_fn, operation_run_fn):
            def inner(*args, **kwargs):
                # Properly set the NR transaction name based on the api function
                # and then call the original operation.run()
                transaction_name = f"{api_fn.__module__}.{api_fn.__name__}"
                newrelic.agent.set_transaction_name(transaction_name)
                return operation_run_fn(*args, **kwargs)

            inner._ttam_nr_wrapped = True
            return inner

        all_urls = NewRelicMiddleware._get_all_resolved_urls()
        for url_pattern in all_urls:
            view_func = url_pattern.callback
            is_ninja_request = isinstance(getattr(view_func, "__self__", None), PathView)
            if not is_ninja_request:
                continue

            path_view: PathView = view_func.__self__
            ninja_operation: Operation
            for ninja_operation in path_view.operations:
                if getattr(ninja_operation.run, "_ttam_nr_wrapped", False):  # dont double-wrap the run function
                    continue

                # This is the actual endpoint fn that we care about
                api_function = ninja_operation.view_func

                # Wrap ninja's Operation.run() function so that set the NR transaction name before it runs
                ninja_operation.run = nr_wrapper(api_function, ninja_operation.run)

    @classmethod
    def _get_all_resolved_urls(cls, url_patterns=None):
        """Recursive function that gets all URLs registered with Django"""
        if url_patterns is None:
            # Start with the top level url patterns
            url_patterns = get_resolver().url_patterns

        url_patterns_resolved = []
        for entry in url_patterns:
            if hasattr(entry, "url_patterns"):
                url_patterns_resolved += cls._get_all_resolved_urls(entry.url_patterns)
            else:
                url_patterns_resolved.append(entry)
        return url_patterns_resolved

This is kind of a gross hack though :) @vitalik do you have an ideas on how we could fix this in the library?

Edit: updated the example, because there was a bug in the original implementation

bqumsiyeh avatar Sep 08 '23 16:09 bqumsiyeh

My first attempt was to try using Newrelic's newrelic.agent.web_transaction decorator, but that didn't seem to work consistently. My workaround to this problem was to write my own decorator. You have to decorate each function, but I only have 4, so this is the simplest approach for me.

...

import functools
import newrelic.agent

...

def instrument_endpoint(view):
    @functools.wraps(view)
    async def wrapped_view(*args, **kwargs):
        transaction_name = f"{view.__module__}.{view.__name__}"
        newrelic.agent.set_transaction_name(transaction_name)
        return await view(*args, **kwargs)

    return wrapped_view

...

@api.get('/{cal:cal}/{year:year}/{month:month}/{day:day}/', response=DaySchema)
@instrument_endpoint
async def get_calendar_day(request, cal: Calendar, year: year, month: month, day: day):
    ...

brianglass avatar Oct 12 '23 18:10 brianglass

any update or ETA for this ?

vkmasters avatar Feb 01 '24 10:02 vkmasters