-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Feature/task panel #2235
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
base: main
Are you sure you want to change the base?
Feature/task panel #2235
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| from django.utils.translation import gettext_lazy as _, ngettext | ||
|
|
||
| from debug_toolbar.panels import Panel | ||
|
|
||
|
|
||
| class TasksPanel(Panel): | ||
| """ | ||
| Panel that displays Django tasks queued or executed during the | ||
| processing of the request. | ||
| """ | ||
|
|
||
| title = _("Tasks") | ||
| template = "debug_toolbar/panels/tasks.html" | ||
|
|
||
| is_async = True | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| super().__init__(*args, **kwargs) | ||
| self.queued_tasks = [] | ||
|
|
||
| @property | ||
| def nav_subtitle(self): | ||
| num_tasks = self.get_stats()["total_tasks"] | ||
| return ngettext( | ||
| "%(num_tasks)d task enqueued", | ||
| "%(num_tasks)d tasks enqueued", | ||
| num_tasks, | ||
| ) % {"num_tasks": num_tasks} | ||
|
|
||
| def generate_stats(self, request, response): | ||
| stats = {"tasks": self.queued_tasks, "total_tasks": len(self.queued_tasks)} | ||
|
|
||
| self.record_stats(stats) | ||
|
|
||
| def enable_instrumentation(self): | ||
| """Hook into task system to collect queued tasks""" | ||
| try: | ||
| import django | ||
|
|
||
| if django.VERSION < (6, 0): | ||
| return | ||
| from django.tasks import Task | ||
|
|
||
| print("[TasksPanel] instrumentation enabled:", hasattr(Task, "enqueue")) | ||
|
|
||
| # Store original enqueue method | ||
| if hasattr(Task, "enqueue"): | ||
| self._original_enqueue = Task.enqueue | ||
|
|
||
| def wrapped_enqueue(task, *args, **kwargs): | ||
| result = self._original_enqueue(task, *args, **kwargs).return_value | ||
| self._record_task(task, args, kwargs, result) | ||
| return result | ||
|
|
||
| Task.enqueue = wrapped_enqueue | ||
| except (ImportError, AttributeError): | ||
| pass | ||
|
|
||
| def _record_task(self, task, args, kwargs, result): | ||
| """Record a task that was queued""" | ||
| task_info = { | ||
| "name": getattr(task, "__name__", str(task)), | ||
| "args": repr(args) if args else "", | ||
| "kwargs": repr(kwargs) if kwargs else "", | ||
| } | ||
| self.queued_tasks.append(task_info) | ||
|
|
||
| def disable_instrumentation(self): | ||
| """Restore original methods""" | ||
| try: | ||
| from django.tasks import Task | ||
|
|
||
| if hasattr(self, "_original_enqueue"): | ||
| Task.enqueue = self._original_enqueue | ||
| except (ImportError, AttributeError): | ||
| pass | ||
|
|
||
| def _check_tasks_available(self): | ||
| """Check if Django tasks system is available""" | ||
| try: | ||
| import django | ||
|
|
||
| if django.VERSION < (6, 0): | ||
| return False | ||
| return True | ||
| except (ImportError, AttributeError): | ||
| return False | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this to be used in the |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,7 @@ def get_config(): | |
| "debug_toolbar.panels.cache.CachePanel", | ||
| "debug_toolbar.panels.signals.SignalsPanel", | ||
| "debug_toolbar.panels.community.CommunityPanel", | ||
| "debug_toolbar.panels.tasks.TasksPanel", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tim-schilling I wonder if it would make sense to check for the Django version here, and only include it if its on a Django version that supports the tasks? |
||
| "debug_toolbar.panels.redirects.RedirectsPanel", | ||
| "debug_toolbar.panels.profiling.ProfilingPanel", | ||
| ] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <table> | ||
| <thead> | ||
| <tr> | ||
| <th>Task Info</th> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will need to be wrapped for translation :-) |
||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {% for task in tasks %} | ||
| <tr> | ||
| <td>{{ task }}</td> | ||
| </tr> | ||
| {% endfor %} | ||
| </tbody> | ||
| </table> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| try: | ||
| from django.tasks import task | ||
| except ImportError: | ||
| # Define a fallback decorator | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the cases where this could happen? I'm guessing just in older django versions? Is a fallback decorator a common pattern in this case? I think if its an example meant to work with tasks it shouldn't need a fallback, but I'd be interested to hear more of your thought process. :-) |
||
| def task(func=None, **kwargs): | ||
| def decorator(f): | ||
| return f | ||
|
|
||
| return decorator if func is None else decorator(func) | ||
|
|
||
|
|
||
| @task | ||
| def send_welcome_message(message): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest setting up some tasks that follow the Django examples tasks closely: https://docs.djangoproject.com/en/dev/topics/tasks/ That will help it feel a bit more 'real-world' and may inspire further ideas for functionality in the panel itself :-) |
||
| return f"Sent message: {message}" | ||
|
|
||
|
|
||
| @task | ||
| def generate_report(report_id): | ||
| return f"Report {report_id} generated" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import asyncio | ||
|
|
||
| import django | ||
| from asgiref.sync import sync_to_async | ||
| from django.contrib.auth.models import User | ||
| from django.http import JsonResponse | ||
|
|
@@ -20,6 +21,13 @@ def jinja2_view(request): | |
|
|
||
|
|
||
| async def async_home(request): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not super familiar with this codebase, but when does this view get used? I'd defer to @tim-schilling and others, but it may make sense to have a dedicated view for triggering off example tasks vs, say, triggering every time you visit an example home page. That could also be a bit more self-documenting and give a place to build off of for the future (like testing larger queues, reporting results on a page template, etc) |
||
| if django.VERSION >= (6, 0): | ||
| from .async_.tasks import generate_report, send_welcome_message | ||
|
|
||
| # Queue some tasks | ||
| send_welcome_message.enqueue(message="hi there") | ||
| generate_report.enqueue(report_id=456) | ||
|
|
||
| return await sync_to_async(render)(request, "index.html") | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This task wrapping looks great. That seems to be the main bits needed to collect stats for the toolbar panel. Nice!