django-cfg 1.4.107__py3-none-any.whl → 1.4.109__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/views/profile.py +19 -9
- django_cfg/apps/centrifugo/views/admin_api.py +4 -7
- django_cfg/apps/centrifugo/views/monitoring.py +3 -6
- django_cfg/apps/centrifugo/views/testing_api.py +3 -6
- django_cfg/apps/dashboard/services/system_health_service.py +16 -11
- django_cfg/apps/dashboard/views/activity_views.py +3 -5
- django_cfg/apps/dashboard/views/apizones_views.py +4 -5
- django_cfg/apps/dashboard/views/charts_views.py +4 -5
- django_cfg/apps/dashboard/views/overview_views.py +4 -5
- django_cfg/apps/dashboard/views/statistics_views.py +4 -5
- django_cfg/apps/dashboard/views/system_views.py +4 -5
- django_cfg/apps/knowbase/__init__.py +2 -2
- django_cfg/apps/knowbase/apps.py +2 -8
- django_cfg/apps/knowbase/views/base.py +9 -4
- django_cfg/apps/support/views/api.py +16 -7
- django_cfg/apps/tasks/__init__.py +61 -2
- django_cfg/apps/tasks/admin/__init__.py +3 -10
- django_cfg/apps/tasks/admin/config.py +98 -0
- django_cfg/apps/tasks/admin/task_log.py +265 -0
- django_cfg/apps/tasks/apps.py +7 -9
- django_cfg/apps/tasks/filters/__init__.py +10 -0
- django_cfg/apps/tasks/filters/task_log.py +121 -0
- django_cfg/apps/tasks/migrations/0001_initial.py +196 -0
- django_cfg/apps/tasks/models/__init__.py +4 -0
- django_cfg/apps/tasks/models/task_log.py +246 -0
- django_cfg/apps/tasks/serializers/__init__.py +28 -0
- django_cfg/apps/tasks/serializers/task_log.py +249 -0
- django_cfg/apps/tasks/services/__init__.py +10 -0
- django_cfg/apps/tasks/services/client/__init__.py +7 -0
- django_cfg/apps/tasks/services/client/client.py +234 -0
- django_cfg/apps/tasks/services/config_helper.py +63 -0
- django_cfg/apps/tasks/services/sync.py +204 -0
- django_cfg/apps/tasks/urls.py +7 -13
- django_cfg/apps/tasks/views/__init__.py +4 -10
- django_cfg/apps/tasks/views/task_log.py +41 -0
- django_cfg/apps/tasks/views/task_log_base.py +41 -0
- django_cfg/apps/tasks/views/task_log_overview.py +100 -0
- django_cfg/apps/tasks/views/task_log_related.py +41 -0
- django_cfg/apps/tasks/views/task_log_stats.py +91 -0
- django_cfg/apps/tasks/views/task_log_timeline.py +81 -0
- django_cfg/apps/urls.py +0 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/utils.py +1 -1
- django_cfg/core/base/config_model.py +1 -1
- django_cfg/core/builders/apps_builder.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/tasks.py +14 -18
- django_cfg/core/generation/security_generators/crypto_fields.py +2 -1
- django_cfg/core/integration/display/startup.py +1 -1
- django_cfg/mixins/__init__.py +12 -0
- django_cfg/mixins/admin_api.py +37 -0
- django_cfg/mixins/client_api.py +39 -0
- django_cfg/models/django/constance.py +2 -8
- django_cfg/models/django/crypto_fields.py +13 -48
- django_cfg/models/tasks/__init__.py +8 -10
- django_cfg/models/tasks/backends.py +76 -207
- django_cfg/models/tasks/config.py +20 -127
- django_cfg/models/tasks/utils.py +17 -29
- django_cfg/modules/django_client/management/commands/generate_client.py +13 -1
- django_cfg/modules/django_unfold/navigation.py +121 -22
- django_cfg/pyproject.toml +2 -2
- django_cfg/registry/core.py +1 -1
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/METADATA +3 -3
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/RECORD +70 -117
- django_cfg/apps/tasks/admin/actions.py +0 -29
- django_cfg/apps/tasks/admin/tasks_admin.py +0 -154
- django_cfg/apps/tasks/api/serializers.py +0 -82
- django_cfg/apps/tasks/api/views.py +0 -571
- django_cfg/apps/tasks/serializers.py +0 -82
- django_cfg/apps/tasks/static/tasks/css/dashboard-alpine.css +0 -299
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +0 -120
- django_cfg/apps/tasks/static/tasks/js/alpine/README.md +0 -47
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/index.js +0 -8
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/management.js +0 -123
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/pagination.js +0 -21
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/tasks.js +0 -101
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/workers.js +0 -59
- django_cfg/apps/tasks/static/tasks/js/alpine/computed.js +0 -35
- django_cfg/apps/tasks/static/tasks/js/alpine/index.js +0 -148
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/index.js +0 -36
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/overview.js +0 -37
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/queues.js +0 -27
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/tasks.js +0 -32
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/workers.js +0 -21
- django_cfg/apps/tasks/static/tasks/js/alpine/state.js +0 -36
- django_cfg/apps/tasks/static/tasks/js/alpine/utils/formatters.js +0 -42
- django_cfg/apps/tasks/static/tasks/js/alpine/utils/helpers.js +0 -68
- django_cfg/apps/tasks/static/tasks/js/dashboard-alpine.js +0 -725
- django_cfg/apps/tasks/tasks/__init__.py +0 -10
- django_cfg/apps/tasks/tasks/demo_tasks.py +0 -127
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +0 -71
- django_cfg/apps/tasks/templates/tasks/components/overview_content.html +0 -94
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +0 -44
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +0 -45
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -151
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +0 -61
- django_cfg/apps/tasks/templates/tasks/components/tasks_mjs_integration.html +0 -269
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +0 -60
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -20
- django_cfg/apps/tasks/templates/tasks/pages/dashboard-improved.html +0 -168
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +0 -77
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +0 -40
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +0 -40
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +0 -86
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +0 -90
- django_cfg/apps/tasks/urls_admin.py +0 -15
- django_cfg/apps/tasks/utils/__init__.py +0 -1
- django_cfg/apps/tasks/utils/simulator.py +0 -353
- django_cfg/apps/tasks/views/api.py +0 -571
- django_cfg/apps/tasks/views/dashboard.py +0 -89
- django_cfg/management/commands/rundramatiq.py +0 -24
- django_cfg/management/commands/rundramatiq_simulator.py +0 -22
- django_cfg/management/commands/task_clear.py +0 -25
- django_cfg/management/commands/task_status.py +0 -24
- django_cfg/modules/django_client/system/__init__.py +0 -24
- django_cfg/modules/django_client/system/base_generator.py +0 -123
- django_cfg/modules/django_client/system/generate_mjs_clients.py +0 -176
- django_cfg/modules/django_client/system/mjs_generator.py +0 -219
- django_cfg/modules/django_client/system/schema_parser.py +0 -199
- django_cfg/modules/django_client/system/templates/api_client.js.j2 +0 -87
- django_cfg/modules/django_client/system/templates/app_index.js.j2 +0 -13
- django_cfg/modules/django_client/system/templates/base_client.js.j2 +0 -166
- django_cfg/modules/django_client/system/templates/main_index.js.j2 +0 -80
- django_cfg/modules/django_client/system/templates/types.js.j2 +0 -24
- django_cfg/modules/django_tasks/__init__.py +0 -29
- django_cfg/modules/django_tasks/dramatiq_setup.py +0 -20
- django_cfg/modules/django_tasks/factory.py +0 -127
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +0 -253
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +0 -436
- django_cfg/modules/django_tasks/management/commands/task_clear.py +0 -226
- django_cfg/modules/django_tasks/management/commands/task_status.py +0 -257
- django_cfg/modules/django_tasks/service.py +0 -281
- django_cfg/modules/django_tasks/settings.py +0 -107
- /django_cfg/{modules/django_tasks/management → apps/tasks/migrations}/__init__.py +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ReArq to TaskLog sync utilities.
|
|
3
|
+
|
|
4
|
+
Helpers for syncing data from ReArq (Tortoise ORM) to TaskLog (Django ORM).
|
|
5
|
+
"""
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from ..models import TaskLog
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_task_log_from_job(
|
|
12
|
+
job_id: str,
|
|
13
|
+
task_name: str,
|
|
14
|
+
queue_name: str = "default",
|
|
15
|
+
args: list = None,
|
|
16
|
+
kwargs: dict = None,
|
|
17
|
+
job_retry: int = 0,
|
|
18
|
+
job_retry_after: int = 60,
|
|
19
|
+
enqueue_time = None,
|
|
20
|
+
expire_time = None,
|
|
21
|
+
status: str = "queued",
|
|
22
|
+
user=None,
|
|
23
|
+
) -> TaskLog:
|
|
24
|
+
"""
|
|
25
|
+
Create TaskLog entry from ReArq Job data.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
job_id: Unique job identifier from ReArq
|
|
29
|
+
task_name: Name of the task function
|
|
30
|
+
queue_name: Queue name (default: "default")
|
|
31
|
+
args: Positional arguments
|
|
32
|
+
kwargs: Keyword arguments
|
|
33
|
+
job_retry: Max retry count from task definition
|
|
34
|
+
job_retry_after: Seconds to wait before retry
|
|
35
|
+
enqueue_time: When job was enqueued (Job.enqueue_time)
|
|
36
|
+
expire_time: When job will expire (Job.expire_time)
|
|
37
|
+
status: Job status (default: "queued")
|
|
38
|
+
user: Django User who triggered the task
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Created TaskLog instance
|
|
42
|
+
"""
|
|
43
|
+
return TaskLog.objects.create(
|
|
44
|
+
job_id=job_id,
|
|
45
|
+
task_name=task_name,
|
|
46
|
+
queue_name=queue_name,
|
|
47
|
+
args=args or [],
|
|
48
|
+
kwargs=kwargs or {},
|
|
49
|
+
job_retry=job_retry,
|
|
50
|
+
job_retry_after=job_retry_after,
|
|
51
|
+
enqueue_time=enqueue_time or timezone.now(),
|
|
52
|
+
expire_time=expire_time,
|
|
53
|
+
status=status,
|
|
54
|
+
user=user,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def update_task_log_from_result(
|
|
59
|
+
job_id: str,
|
|
60
|
+
worker: str,
|
|
61
|
+
success: bool,
|
|
62
|
+
result: str = None,
|
|
63
|
+
start_time = None,
|
|
64
|
+
finish_time = None,
|
|
65
|
+
) -> Optional[TaskLog]:
|
|
66
|
+
"""
|
|
67
|
+
Update TaskLog with JobResult data.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
job_id: Job identifier
|
|
71
|
+
worker: Worker that processed the task
|
|
72
|
+
success: Whether task succeeded
|
|
73
|
+
result: Task result (JSON string)
|
|
74
|
+
start_time: When execution started (JobResult.start_time)
|
|
75
|
+
finish_time: When execution finished (JobResult.finish_time)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Updated TaskLog instance or None if not found
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
task_log = TaskLog.objects.get(job_id=job_id)
|
|
82
|
+
except TaskLog.DoesNotExist:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Update fields from JobResult
|
|
86
|
+
task_log.worker_id = worker
|
|
87
|
+
task_log.success = success
|
|
88
|
+
task_log.start_time = start_time
|
|
89
|
+
task_log.finish_time = finish_time
|
|
90
|
+
|
|
91
|
+
if result is not None:
|
|
92
|
+
task_log.result = result
|
|
93
|
+
|
|
94
|
+
# Calculate duration
|
|
95
|
+
if start_time and finish_time:
|
|
96
|
+
delta = finish_time - start_time
|
|
97
|
+
task_log.duration_ms = int(delta.total_seconds() * 1000)
|
|
98
|
+
|
|
99
|
+
# Update status based on success
|
|
100
|
+
if success:
|
|
101
|
+
task_log.status = TaskLog.StatusChoices.SUCCESS
|
|
102
|
+
else:
|
|
103
|
+
task_log.status = TaskLog.StatusChoices.FAILED
|
|
104
|
+
|
|
105
|
+
task_log.save()
|
|
106
|
+
return task_log
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def sync_job_status(job_id: str, status: str, job_retries: int = None) -> Optional[TaskLog]:
|
|
110
|
+
"""
|
|
111
|
+
Sync Job status to TaskLog.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
job_id: Job identifier
|
|
115
|
+
status: ReArq JobStatus value
|
|
116
|
+
job_retries: Current retry count
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Updated TaskLog instance or None if not found
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
task_log = TaskLog.objects.get(job_id=job_id)
|
|
123
|
+
except TaskLog.DoesNotExist:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
task_log.status = status
|
|
127
|
+
if job_retries is not None:
|
|
128
|
+
task_log.job_retries = job_retries
|
|
129
|
+
|
|
130
|
+
task_log.save(update_fields=["status", "job_retries", "updated_at"])
|
|
131
|
+
return task_log
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def sync_from_rearq_job(rearq_job):
|
|
135
|
+
"""
|
|
136
|
+
Sync TaskLog from ReArq Job instance (Tortoise model).
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
from rearq.server.models import Job
|
|
140
|
+
job = await Job.get(job_id="...")
|
|
141
|
+
await sync_from_rearq_job(job)
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
rearq_job: ReArq Job instance (Tortoise ORM)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
TaskLog instance
|
|
148
|
+
"""
|
|
149
|
+
from asgiref.sync import sync_to_async
|
|
150
|
+
|
|
151
|
+
# Extract queue name from full queue path
|
|
152
|
+
queue_name = rearq_job.task.split(':')[-1] if ':' in rearq_job.task else "default"
|
|
153
|
+
|
|
154
|
+
# Check if TaskLog exists
|
|
155
|
+
task_log = await sync_to_async(TaskLog.objects.filter(job_id=rearq_job.job_id).first)()
|
|
156
|
+
|
|
157
|
+
if task_log:
|
|
158
|
+
# Update existing
|
|
159
|
+
task_log.status = rearq_job.status
|
|
160
|
+
task_log.job_retries = rearq_job.job_retries
|
|
161
|
+
await sync_to_async(task_log.save)(update_fields=["status", "job_retries", "updated_at"])
|
|
162
|
+
else:
|
|
163
|
+
# Create new
|
|
164
|
+
task_log = await sync_to_async(create_task_log_from_job)(
|
|
165
|
+
job_id=rearq_job.job_id,
|
|
166
|
+
task_name=rearq_job.task,
|
|
167
|
+
queue_name=queue_name,
|
|
168
|
+
args=rearq_job.args or [],
|
|
169
|
+
kwargs=rearq_job.kwargs or {},
|
|
170
|
+
job_retry=rearq_job.job_retry,
|
|
171
|
+
job_retry_after=rearq_job.job_retry_after,
|
|
172
|
+
enqueue_time=rearq_job.enqueue_time,
|
|
173
|
+
expire_time=rearq_job.expire_time,
|
|
174
|
+
status=rearq_job.status,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return task_log
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def sync_from_rearq_result(rearq_result):
|
|
181
|
+
"""
|
|
182
|
+
Sync TaskLog from ReArq JobResult instance (Tortoise model).
|
|
183
|
+
|
|
184
|
+
Usage:
|
|
185
|
+
from rearq.server.models import JobResult
|
|
186
|
+
result = await JobResult.get(job_id="...").prefetch_related("job")
|
|
187
|
+
await sync_from_rearq_result(result)
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
rearq_result: ReArq JobResult instance (Tortoise ORM)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
TaskLog instance or None
|
|
194
|
+
"""
|
|
195
|
+
from asgiref.sync import sync_to_async
|
|
196
|
+
|
|
197
|
+
return await sync_to_async(update_task_log_from_result)(
|
|
198
|
+
job_id=rearq_result.job_id,
|
|
199
|
+
worker=rearq_result.worker,
|
|
200
|
+
success=rearq_result.success,
|
|
201
|
+
result=rearq_result.result,
|
|
202
|
+
start_time=rearq_result.start_time,
|
|
203
|
+
finish_time=rearq_result.finish_time,
|
|
204
|
+
)
|
django_cfg/apps/tasks/urls.py
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
ReArq Tasks API URLs.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
REST API endpoints for task monitoring and management.
|
|
5
5
|
"""
|
|
6
|
-
|
|
7
|
-
from django.urls import include, path
|
|
6
|
+
from django.urls import path
|
|
8
7
|
from rest_framework.routers import DefaultRouter
|
|
9
8
|
|
|
10
|
-
from .views import
|
|
9
|
+
from .views import TaskLogViewSet
|
|
11
10
|
|
|
12
|
-
app_name = '
|
|
11
|
+
app_name = 'django_cfg_tasks'
|
|
13
12
|
|
|
14
|
-
# Main router for ViewSets
|
|
15
13
|
router = DefaultRouter()
|
|
16
|
-
router.register(r'',
|
|
17
|
-
|
|
18
|
-
urlpatterns = [
|
|
19
|
-
# RESTful API endpoints using ViewSets
|
|
20
|
-
path('api/', include(router.urls)),
|
|
14
|
+
router.register(r'logs', TaskLogViewSet, basename='tasklog')
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
urlpatterns = router.urls
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
ReArq Tasks API Views.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
- api: DRF ViewSets for API endpoints
|
|
6
|
-
- dashboard: Dashboard template views
|
|
7
|
-
- base: Shared functionality and mixins
|
|
4
|
+
ViewSets for task monitoring and management.
|
|
8
5
|
"""
|
|
9
|
-
|
|
10
|
-
from .api import TaskManagementViewSet
|
|
11
|
-
from .dashboard import dashboard_view
|
|
6
|
+
from .task_log import TaskLogViewSet
|
|
12
7
|
|
|
13
8
|
__all__ = [
|
|
14
|
-
|
|
15
|
-
'dashboard_view'
|
|
9
|
+
"TaskLogViewSet",
|
|
16
10
|
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog ViewSet - Main Entry Point.
|
|
3
|
+
|
|
4
|
+
Combines all task log functionality through mixins.
|
|
5
|
+
"""
|
|
6
|
+
from .task_log_base import TaskLogBaseViewSet
|
|
7
|
+
from .task_log_stats import TaskLogStatsMixin
|
|
8
|
+
from .task_log_timeline import TaskLogTimelineMixin
|
|
9
|
+
from .task_log_overview import TaskLogOverviewMixin
|
|
10
|
+
from .task_log_related import TaskLogRelatedMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskLogViewSet(
|
|
14
|
+
TaskLogStatsMixin,
|
|
15
|
+
TaskLogTimelineMixin,
|
|
16
|
+
TaskLogOverviewMixin,
|
|
17
|
+
TaskLogRelatedMixin,
|
|
18
|
+
TaskLogBaseViewSet,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Complete ViewSet for TaskLog monitoring.
|
|
22
|
+
|
|
23
|
+
Provides read-only access to task execution logs with filtering,
|
|
24
|
+
searching, and statistics.
|
|
25
|
+
|
|
26
|
+
Endpoints:
|
|
27
|
+
GET /api/tasks/logs/ - List all task logs
|
|
28
|
+
GET /api/tasks/logs/{id}/ - Get task log details
|
|
29
|
+
GET /api/tasks/logs/{id}/related/ - Get related task logs
|
|
30
|
+
GET /api/tasks/logs/stats/ - Get aggregated statistics
|
|
31
|
+
GET /api/tasks/logs/timeline/ - Get task execution timeline
|
|
32
|
+
GET /api/tasks/logs/overview/ - Get summary overview
|
|
33
|
+
|
|
34
|
+
Mixins:
|
|
35
|
+
- TaskLogStatsMixin: Aggregated statistics
|
|
36
|
+
- TaskLogTimelineMixin: Time-series data
|
|
37
|
+
- TaskLogOverviewMixin: High-level summary
|
|
38
|
+
- TaskLogRelatedMixin: Related task lookup
|
|
39
|
+
- TaskLogBaseViewSet: Base CRUD operations
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog Base ViewSet.
|
|
3
|
+
|
|
4
|
+
Base viewset with common configuration for task log endpoints.
|
|
5
|
+
"""
|
|
6
|
+
from rest_framework import viewsets
|
|
7
|
+
from rest_framework.filters import SearchFilter, OrderingFilter
|
|
8
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
|
9
|
+
|
|
10
|
+
from django_cfg.mixins import AdminAPIMixin
|
|
11
|
+
from ..models import TaskLog
|
|
12
|
+
from ..serializers import (
|
|
13
|
+
TaskLogSerializer,
|
|
14
|
+
TaskLogListSerializer,
|
|
15
|
+
TaskLogDetailSerializer,
|
|
16
|
+
)
|
|
17
|
+
from ..filters import TaskLogFilter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TaskLogBaseViewSet(AdminAPIMixin, viewsets.ReadOnlyModelViewSet):
|
|
21
|
+
"""
|
|
22
|
+
Base ViewSet for TaskLog monitoring.
|
|
23
|
+
|
|
24
|
+
Provides read-only access to task execution logs with filtering and searching.
|
|
25
|
+
Extended by mixins for additional functionality (stats, timeline, overview).
|
|
26
|
+
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
queryset = TaskLog.objects.all().order_by('-enqueue_time')
|
|
30
|
+
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
|
31
|
+
filterset_class = TaskLogFilter
|
|
32
|
+
search_fields = ['task_name', 'job_id', 'queue_name', 'error_message', 'worker_id']
|
|
33
|
+
ordering_fields = ['enqueue_time', 'start_time', 'finish_time', 'duration_ms', 'job_retries', 'created_at']
|
|
34
|
+
|
|
35
|
+
def get_serializer_class(self):
|
|
36
|
+
"""Return appropriate serializer based on action."""
|
|
37
|
+
if self.action == 'list':
|
|
38
|
+
return TaskLogListSerializer
|
|
39
|
+
elif self.action == 'retrieve':
|
|
40
|
+
return TaskLogDetailSerializer
|
|
41
|
+
return TaskLogSerializer
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog Overview Actions.
|
|
3
|
+
|
|
4
|
+
Provides high-level summary statistics for the entire task system.
|
|
5
|
+
"""
|
|
6
|
+
from django.db.models import Q, Count
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from drf_spectacular.utils import extend_schema
|
|
10
|
+
from rest_framework.decorators import action
|
|
11
|
+
from rest_framework.response import Response
|
|
12
|
+
|
|
13
|
+
from ..models import TaskLog
|
|
14
|
+
from ..serializers import TaskLogOverviewSerializer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskLogOverviewMixin:
|
|
18
|
+
"""
|
|
19
|
+
Mixin for task system overview endpoints.
|
|
20
|
+
|
|
21
|
+
Provides high-level statistics:
|
|
22
|
+
- Total task counts
|
|
23
|
+
- Active queues
|
|
24
|
+
- Recent failures
|
|
25
|
+
- Distribution by queue and status
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@extend_schema(
|
|
29
|
+
responses={200: TaskLogOverviewSerializer},
|
|
30
|
+
summary="Task System Overview",
|
|
31
|
+
description="Get high-level summary statistics for the entire task system"
|
|
32
|
+
)
|
|
33
|
+
@action(detail=False, methods=['get'])
|
|
34
|
+
def overview(self, request):
|
|
35
|
+
"""
|
|
36
|
+
Get summary overview of task system.
|
|
37
|
+
|
|
38
|
+
Returns properly structured data validated by TaskLogOverviewSerializer.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
{
|
|
42
|
+
"total_tasks": 1500,
|
|
43
|
+
"active_queues": ["default", "high", "knowledge"],
|
|
44
|
+
"recent_failures": 5,
|
|
45
|
+
"tasks_by_queue": [
|
|
46
|
+
{"queue_name": "default", "count": 800},
|
|
47
|
+
{"queue_name": "high", "count": 500},
|
|
48
|
+
{"queue_name": "knowledge", "count": 200}
|
|
49
|
+
],
|
|
50
|
+
"tasks_by_status": [
|
|
51
|
+
{"status": "success", "count": 1450},
|
|
52
|
+
{"status": "failed", "count": 45},
|
|
53
|
+
{"status": "in_progress", "count": 5}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
# Get all-time statistics
|
|
58
|
+
total_tasks = TaskLog.objects.count()
|
|
59
|
+
|
|
60
|
+
# Get active queues
|
|
61
|
+
active_queues = list(
|
|
62
|
+
TaskLog.objects.values_list('queue_name', flat=True)
|
|
63
|
+
.distinct()
|
|
64
|
+
.order_by('queue_name')
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Recent failures (last 24 hours)
|
|
68
|
+
time_threshold = timezone.now() - timedelta(hours=24)
|
|
69
|
+
recent_failures = TaskLog.objects.filter(
|
|
70
|
+
Q(success=False) | Q(status__in=['failed', 'expired']),
|
|
71
|
+
created_at__gte=time_threshold
|
|
72
|
+
).count()
|
|
73
|
+
|
|
74
|
+
# Tasks by queue - convert to array of objects
|
|
75
|
+
tasks_by_queue = [
|
|
76
|
+
{'queue_name': item['queue_name'], 'count': item['count']}
|
|
77
|
+
for item in TaskLog.objects.values('queue_name')
|
|
78
|
+
.annotate(count=Count('id'))
|
|
79
|
+
.order_by('queue_name')
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Tasks by status - convert to array of objects
|
|
83
|
+
tasks_by_status = [
|
|
84
|
+
{'status': item['status'], 'count': item['count']}
|
|
85
|
+
for item in TaskLog.objects.values('status')
|
|
86
|
+
.annotate(count=Count('id'))
|
|
87
|
+
.order_by('status')
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
overview_data = {
|
|
91
|
+
'total_tasks': total_tasks,
|
|
92
|
+
'active_queues': active_queues,
|
|
93
|
+
'recent_failures': recent_failures,
|
|
94
|
+
'tasks_by_queue': tasks_by_queue,
|
|
95
|
+
'tasks_by_status': tasks_by_status,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Validate and serialize data
|
|
99
|
+
serializer = TaskLogOverviewSerializer(overview_data)
|
|
100
|
+
return Response(serializer.data)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog Related Tasks Actions.
|
|
3
|
+
|
|
4
|
+
Provides endpoints for finding related task executions.
|
|
5
|
+
"""
|
|
6
|
+
from django.db.models import Q
|
|
7
|
+
from rest_framework.decorators import action
|
|
8
|
+
from rest_framework.response import Response
|
|
9
|
+
|
|
10
|
+
from ..models import TaskLog
|
|
11
|
+
from ..serializers import TaskLogListSerializer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskLogRelatedMixin:
|
|
15
|
+
"""
|
|
16
|
+
Mixin for related tasks endpoints.
|
|
17
|
+
|
|
18
|
+
Provides functionality to find related task executions:
|
|
19
|
+
- Same job_id (retries)
|
|
20
|
+
- Same task_name (similar executions)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@action(detail=True, methods=['get'])
|
|
24
|
+
def related(self, request, pk=None):
|
|
25
|
+
"""
|
|
26
|
+
Get related task logs (same job_id or task_name).
|
|
27
|
+
|
|
28
|
+
Returns tasks that share the same job_id or are retries of the same task.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Array of related TaskLog objects (up to 10 most recent)
|
|
32
|
+
"""
|
|
33
|
+
task_log = self.get_object()
|
|
34
|
+
|
|
35
|
+
# Find related tasks
|
|
36
|
+
related = TaskLog.objects.filter(
|
|
37
|
+
Q(job_id=task_log.job_id) | Q(task_name=task_log.task_name)
|
|
38
|
+
).exclude(id=task_log.id).order_by('-created_at')[:10]
|
|
39
|
+
|
|
40
|
+
serializer = TaskLogListSerializer(related, many=True)
|
|
41
|
+
return Response(serializer.data)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog Statistics Actions.
|
|
3
|
+
|
|
4
|
+
Provides aggregated statistics and metrics for task execution monitoring.
|
|
5
|
+
"""
|
|
6
|
+
from django.db.models import Q, Avg
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from drf_spectacular.utils import extend_schema
|
|
10
|
+
from rest_framework.decorators import action
|
|
11
|
+
from rest_framework.response import Response
|
|
12
|
+
|
|
13
|
+
from ..models import TaskLog
|
|
14
|
+
from ..serializers import TaskLogStatsSerializer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskLogStatsMixin:
|
|
18
|
+
"""
|
|
19
|
+
Mixin for task statistics endpoints.
|
|
20
|
+
|
|
21
|
+
Provides aggregated statistics about task execution:
|
|
22
|
+
- Success/failure rates
|
|
23
|
+
- Average durations
|
|
24
|
+
- Task counts by status
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@extend_schema(
|
|
28
|
+
responses={200: TaskLogStatsSerializer},
|
|
29
|
+
summary="Task Execution Statistics",
|
|
30
|
+
description="Get aggregated statistics about task execution (success/failure rates, duration)"
|
|
31
|
+
)
|
|
32
|
+
@action(detail=False, methods=['get'])
|
|
33
|
+
def stats(self, request):
|
|
34
|
+
"""
|
|
35
|
+
Get aggregated task statistics.
|
|
36
|
+
|
|
37
|
+
Query Parameters:
|
|
38
|
+
period_hours (int): Statistics period in hours (default: 24)
|
|
39
|
+
task_name (str): Filter by specific task name
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
{
|
|
43
|
+
"total": 150,
|
|
44
|
+
"successful": 145,
|
|
45
|
+
"failed": 5,
|
|
46
|
+
"in_progress": 2,
|
|
47
|
+
"success_rate": 96.67,
|
|
48
|
+
"avg_duration_ms": 1250,
|
|
49
|
+
"avg_duration_seconds": 1.25,
|
|
50
|
+
"period_hours": 24
|
|
51
|
+
}
|
|
52
|
+
"""
|
|
53
|
+
period_hours = int(request.query_params.get('period_hours', 24))
|
|
54
|
+
task_name = request.query_params.get('task_name')
|
|
55
|
+
|
|
56
|
+
# Calculate time threshold
|
|
57
|
+
time_threshold = timezone.now() - timedelta(hours=period_hours)
|
|
58
|
+
|
|
59
|
+
# Base queryset
|
|
60
|
+
queryset = TaskLog.objects.filter(created_at__gte=time_threshold)
|
|
61
|
+
if task_name:
|
|
62
|
+
queryset = queryset.filter(task_name=task_name)
|
|
63
|
+
|
|
64
|
+
# Calculate statistics
|
|
65
|
+
total = queryset.count()
|
|
66
|
+
successful = queryset.filter(success=True).count()
|
|
67
|
+
failed = queryset.filter(success=False, status='failed').count()
|
|
68
|
+
in_progress = queryset.filter(Q(status='in_progress') | Q(status='queued')).count()
|
|
69
|
+
|
|
70
|
+
# Calculate success rate
|
|
71
|
+
completed = successful + failed
|
|
72
|
+
success_rate = (successful / completed * 100) if completed > 0 else 0.0
|
|
73
|
+
|
|
74
|
+
# Calculate average duration (only for completed tasks)
|
|
75
|
+
avg_duration = queryset.filter(
|
|
76
|
+
duration_ms__isnull=False
|
|
77
|
+
).aggregate(avg=Avg('duration_ms'))['avg'] or 0
|
|
78
|
+
|
|
79
|
+
stats_data = {
|
|
80
|
+
'total': total,
|
|
81
|
+
'successful': successful,
|
|
82
|
+
'failed': failed,
|
|
83
|
+
'in_progress': in_progress,
|
|
84
|
+
'success_rate': round(success_rate, 2),
|
|
85
|
+
'avg_duration_ms': int(avg_duration),
|
|
86
|
+
'avg_duration_seconds': round(avg_duration / 1000, 2),
|
|
87
|
+
'period_hours': period_hours,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
serializer = TaskLogStatsSerializer(stats_data)
|
|
91
|
+
return Response(serializer.data)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog Timeline Actions.
|
|
3
|
+
|
|
4
|
+
Provides time-series data for task execution visualization.
|
|
5
|
+
"""
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from drf_spectacular.utils import extend_schema
|
|
9
|
+
from rest_framework.decorators import action
|
|
10
|
+
from rest_framework.response import Response
|
|
11
|
+
|
|
12
|
+
from ..models import TaskLog
|
|
13
|
+
from ..serializers import TaskLogTimelineSerializer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TaskLogTimelineMixin:
|
|
17
|
+
"""
|
|
18
|
+
Mixin for task timeline endpoints.
|
|
19
|
+
|
|
20
|
+
Provides time-bucketed statistics for visualization:
|
|
21
|
+
- Hourly/daily task counts
|
|
22
|
+
- Success/failure trends
|
|
23
|
+
- Performance metrics over time
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@extend_schema(
|
|
27
|
+
responses={200: TaskLogTimelineSerializer},
|
|
28
|
+
summary="Task Execution Timeline",
|
|
29
|
+
description="Get time-series data of task executions grouped by time intervals"
|
|
30
|
+
)
|
|
31
|
+
@action(detail=False, methods=['get'])
|
|
32
|
+
def timeline(self, request):
|
|
33
|
+
"""
|
|
34
|
+
Get task execution timeline grouped by time intervals.
|
|
35
|
+
|
|
36
|
+
Returns timeline data wrapped in object with period/interval metadata.
|
|
37
|
+
|
|
38
|
+
Query Parameters:
|
|
39
|
+
period_hours (int): Timeline period in hours (default: 24)
|
|
40
|
+
interval (str): Grouping interval - 'hour', 'day' (default: 'hour')
|
|
41
|
+
task_name (str): Filter by specific task name
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
{
|
|
45
|
+
"period_hours": 24,
|
|
46
|
+
"interval": "hour",
|
|
47
|
+
"data": [
|
|
48
|
+
{
|
|
49
|
+
"timestamp": "2025-10-30T10:00:00Z",
|
|
50
|
+
"total": 15,
|
|
51
|
+
"successful": 14,
|
|
52
|
+
"failed": 1,
|
|
53
|
+
"avg_duration_ms": 1200
|
|
54
|
+
},
|
|
55
|
+
...
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
period_hours = int(request.query_params.get('period_hours', 24))
|
|
60
|
+
interval = request.query_params.get('interval', 'hour')
|
|
61
|
+
task_name = request.query_params.get('task_name')
|
|
62
|
+
|
|
63
|
+
# Calculate time threshold
|
|
64
|
+
time_threshold = timezone.now() - timedelta(hours=period_hours)
|
|
65
|
+
|
|
66
|
+
# Base queryset
|
|
67
|
+
queryset = TaskLog.objects.filter(created_at__gte=time_threshold)
|
|
68
|
+
if task_name:
|
|
69
|
+
queryset = queryset.filter(task_name=task_name)
|
|
70
|
+
|
|
71
|
+
# TODO: Implement proper time-series grouping with SQL time bucketing
|
|
72
|
+
# For now, return empty data array with correct structure
|
|
73
|
+
timeline_data = {
|
|
74
|
+
'period_hours': period_hours,
|
|
75
|
+
'interval': interval,
|
|
76
|
+
'data': [] # Empty for now - will be implemented later
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Validate and serialize
|
|
80
|
+
serializer = TaskLogTimelineSerializer(timeline_data)
|
|
81
|
+
return Response(serializer.data)
|
django_cfg/apps/urls.py
CHANGED
|
@@ -175,7 +175,6 @@ APP_URL_MAP = {
|
|
|
175
175
|
],
|
|
176
176
|
"django_cfg.apps.tasks": [
|
|
177
177
|
("cfg/tasks/", "django_cfg.apps.tasks.urls"),
|
|
178
|
-
("cfg/tasks/admin/", "django_cfg.apps.tasks.urls_admin"),
|
|
179
178
|
],
|
|
180
179
|
"django_cfg.apps.payments": [
|
|
181
180
|
("cfg/payments/", "django_cfg.apps.payments.urls"),
|
django_cfg/cli/commands/info.py
CHANGED
|
@@ -74,7 +74,7 @@ def info(verbose: bool):
|
|
|
74
74
|
"🌐 Service Integrations": ["twilio", "sendgrid", "openai", "telegram-bot-api"],
|
|
75
75
|
"🎨 Admin & UI": ["django-unfold", "django-constance"],
|
|
76
76
|
"📊 API & Documentation": ["djangorestframework", "drf-spectacular"],
|
|
77
|
-
"⚡ Background Processing": ["
|
|
77
|
+
"⚡ Background Processing": ["rearq", "redis"],
|
|
78
78
|
"🛠️ Development Tools": ["ngrok"],
|
|
79
79
|
}
|
|
80
80
|
|
django_cfg/cli/utils.py
CHANGED