From c26df87d809d131574be2bc4a25065dc914e122d Mon Sep 17 00:00:00 2001 From: Aleksandr Meshchriakov Date: Wed, 22 Apr 2026 10:51:00 +0200 Subject: [PATCH] Improve backup admin error visibility and log celery tracebacks --- src/apps/backups/admin.py | 104 ++++++++++++++++++++++++++++++++++++++ src/apps/backups/tasks.py | 4 +- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/apps/backups/admin.py b/src/apps/backups/admin.py index 95721ed..777b119 100644 --- a/src/apps/backups/admin.py +++ b/src/apps/backups/admin.py @@ -3,12 +3,15 @@ from apps.backups.models import BackupExportJob from apps.backups.serializers import BackupExportRequestSerializer from apps.backups.services import BackupExportError, BackupExportJobService +from apps.core.services import BackgroundJobService from django.contrib import admin from django.contrib import messages from django.http import HttpResponse from django.shortcuts import redirect from django.urls import path, reverse +from django.utils.html import format_html from django.utils import timezone +from urllib.parse import urlencode @admin.register(BackupExportJob) @@ -23,9 +26,11 @@ class BackupExportJobAdmin(admin.ModelAdmin): "requested_by", "organizations_count", "archive_size", + "error_short", "started_at", "completed_at", ] + list_display_links = ["id"] list_filter = ["status", "actual_date", "created_at", "completed_at"] search_fields = ["task_id", "checksum_sha256", "archive_filename"] readonly_fields = [ @@ -37,14 +42,113 @@ class BackupExportJobAdmin(admin.ModelAdmin): "archive_size", "organizations_count", "error", + "background_job_link", + "background_job_error", + "background_job_traceback", "started_at", "completed_at", "created_at", "updated_at", ] + fieldsets = ( + ( + "Основные поля", + {"fields": ("actual_date", "status", "task_id", "background_job_link")}, + ), + ( + "Исполнитель", + {"fields": ("requested_by",)}, + ), + ( + "Результат", + { + "fields": ( + "archive_path", + "archive_filename", + "checksum_filename", + "checksum_sha256", + "archive_size", + "organizations_count", + ) + }, + ), + ( + "Ошибка", + { + "fields": ( + "error", + "background_job_error", + "background_job_traceback", + ), + "classes": ("collapse",), + }, + ), + ( + "Время", + {"fields": ("started_at", "completed_at", "created_at", "updated_at")}, + ), + ) ordering = ["-actual_date", "-created_at"] change_list_template = "admin/backups/backupexportjob/change_list.html" + @staticmethod + def _format_error_text(text: str) -> str: + """Сократить и безопасно форматировать текст ошибки для списка.""" + if not text: + return "—" + text = text.strip() + return text if len(text) <= 120 else f"{text[:117]}…" + + def error_short(self, obj: BackupExportJob) -> str: + """Короткий текст ошибки для списка.""" + return self._format_error_text(obj.error) + + error_short.short_description = "Ошибка" + + def _get_background_job(self, obj: BackupExportJob): + if not obj.task_id: + return None + return BackgroundJobService.get_by_task_id_or_none(obj.task_id) + + def background_job_link(self, obj: BackupExportJob): + background_job = self._get_background_job(obj) + if not background_job: + return "—" + changelist_url = ( + reverse("admin:core_backgroundjob_changelist") + + "?" + + urlencode({"q": background_job.task_id}) + ) + return format_html( + 'Открыть в журнале задач: {}', + changelist_url, + background_job.task_id, + ) + + background_job_link.short_description = "Celery задача" + + def background_job_error(self, obj: BackupExportJob) -> str: + background_job = self._get_background_job(obj) + if not background_job: + return "—" + if background_job.error: + return background_job.error + return "—" + + background_job_error.short_description = "Ошибка в Celery" + + def background_job_traceback(self, obj: BackupExportJob): + background_job = self._get_background_job(obj) + if not background_job or not background_job.traceback: + return "—" + return format_html( + '
{}
', + background_job.traceback, + ) + + background_job_traceback.short_description = "Traceback из фоновой задачи" + def get_urls(self): urls = super().get_urls() custom_urls = [ diff --git a/src/apps/backups/tasks.py b/src/apps/backups/tasks.py index c8e057e..1a85273 100644 --- a/src/apps/backups/tasks.py +++ b/src/apps/backups/tasks.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import traceback import uuid from pathlib import Path @@ -109,9 +110,10 @@ def generate_backup_for_date(self, job_id: int) -> dict: job.actual_date, job.id, ) + error_traceback = traceback.format_exc() job.status = BackupExportJob.Status.FAILURE job.error = str(exc) job.completed_at = timezone.now() job.save(update_fields=["status", "error", "completed_at", "updated_at"]) - background_job.fail(error=str(exc)) + background_job.fail(error=str(exc), traceback_str=error_traceback) raise