diff --git a/procrastinate/contrib/django/admin.py b/procrastinate/contrib/django/admin.py index 1abd662b0..fd4ac7c71 100644 --- a/procrastinate/contrib/django/admin.py +++ b/procrastinate/contrib/django/admin.py @@ -4,11 +4,15 @@ from django.contrib import admin from django.db.models import Prefetch +from django.http import JsonResponse from django.template.loader import render_to_string +from django.urls import path, reverse from django.utils import timezone -from django.utils.html import format_html +from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe +from procrastinate.utils import ellipsize_middle + from . import models JOB_STATUS_EMOJI_MAPPING = { @@ -90,6 +94,17 @@ def get_queryset(self, request): ) ) + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "/full_args/", + self.admin_site.admin_view(self.full_args_view), + name="full_args", + ), + ] + return custom_urls + urls + @admin.display(description="Status") def pretty_status(self, instance: models.ProcrastinateJob) -> str: emoji = JOB_STATUS_EMOJI_MAPPING.get(instance.status, "") @@ -106,12 +121,19 @@ def short_task_name(self, instance: models.ProcrastinateJob) -> str: @admin.display(description="Args") def pretty_args(self, instance: models.ProcrastinateJob) -> str: - indent = 2 if len(instance.args) > 1 or len(str(instance.args)) > 30 else None - pretty_json = json.dumps(instance.args, indent=indent) - if len(pretty_json) > 2000: - pretty_json = pretty_json[:2000] + "..." + rows = format_html_join( + "\n", + "{}{}", + ( + (key, ellipsize_middle(json.dumps(value))) + for key, value in instance.args.items() + ), + ) return format_html( - '
{pretty_json}
', pretty_json=pretty_json + "{rows}
" + '
View unformatted
', + rows=rows, + full_args_url=reverse("admin:full_args", kwargs={"job_id": instance.id}), ) @admin.display(description="Summary") @@ -128,3 +150,7 @@ def summary(self, instance: models.ProcrastinateJob) -> str: ).strip() ) return "" + + def full_args_view(self, request, job_id): + instance = models.ProcrastinateJob.objects.get(id=job_id) + return JsonResponse(instance.args) diff --git a/procrastinate/jobs.py b/procrastinate/jobs.py index 3f2f8f1f2..0dab344d8 100644 --- a/procrastinate/jobs.py +++ b/procrastinate/jobs.py @@ -10,6 +10,7 @@ from typing_extensions import Literal from procrastinate import types +from procrastinate.utils import ellipsize_middle if TYPE_CHECKING: from procrastinate import manager @@ -136,7 +137,8 @@ def evolve(self, **kwargs: Any) -> Job: @cached_property def call_string(self): kwargs_string = ", ".join( - f"{key}={value!r}" for key, value in self.task_kwargs.items() + f"{key}={ellipsize_middle(repr(value))}" + for key, value in self.task_kwargs.items() ) return f"{self.task_name}[{self.id}]({kwargs_string})" diff --git a/procrastinate/utils.py b/procrastinate/utils.py index 37d100634..77b0ca9e7 100644 --- a/procrastinate/utils.py +++ b/procrastinate/utils.py @@ -329,3 +329,17 @@ def queues_display(queues: Iterable[str] | None) -> str: return f"queues {', '.join(queues)}" else: return "all queues" + + +def ellipsize_middle( + value: str, max_length: int = 100, suffix_length: int = 20, ellipsis: str = "..." +) -> str: + """ + Limits the length of a string to `max_length` by placing `ellipsis` in the middle, preserving `suffix_length` at the end. + """ + prefix_length = max_length - len(ellipsis) - suffix_length + + if len(value) > max_length: + return value[:prefix_length] + ellipsis + value[-suffix_length:] + else: + return value diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index 3c6d5de8c..99159fa5b 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import json import pytest @@ -47,6 +48,15 @@ def test_job_get_context(job_factory, scheduled_at, context_scheduled_at): } +def test_log_context_does_not_grow_infinitely(job_factory): + large_arg_len = 10**5 + job = job_factory( + task_kwargs={"large_arg": "a" * large_arg_len}, + ) + + assert len(json.dumps(job.log_context())) < large_arg_len + + def test_job_evolve(job_factory): job = job_factory(id=12, task_name="mytask", lock="sher", queue="marsupilami") expected = job_factory(id=13, task_name="mytask", lock="bu", queue="marsupilami")