django-ninja
django-ninja copied to clipboard
properly name functions
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 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?
how did you fix this? I have the same problem with new relic.
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
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):
...
any update or ETA for this ?