diff --git a/netbox/core/tables/jobs.py b/netbox/core/tables/jobs.py index 2406101a1..e97fb0ac4 100644 --- a/netbox/core/tables/jobs.py +++ b/netbox/core/tables/jobs.py @@ -1,4 +1,6 @@ import django_tables2 as tables +from django.utils.html import conditional_escape +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from core.constants import JOB_LOG_ENTRY_LEVELS @@ -82,3 +84,9 @@ class JobLogEntryTable(BaseTable): class Meta(BaseTable.Meta): empty_text = _('No log entries') fields = ('timestamp', 'level', 'message') + + def render_message(self, record, value): + if record.get('level') == 'error' and '\n' in value: + value = conditional_escape(value) + return mark_safe(f'
{value}')
+ return value
diff --git a/netbox/netbox/jobs.py b/netbox/netbox/jobs.py
index 30afba6fc..8942b831f 100644
--- a/netbox/netbox/jobs.py
+++ b/netbox/netbox/jobs.py
@@ -1,4 +1,5 @@
import logging
+import traceback
from abc import ABC, abstractmethod
from datetime import timedelta
@@ -107,6 +108,12 @@ class JobRunner(ABC):
job.terminate(status=JobStatusChoices.STATUS_FAILED)
except Exception as e:
+ tb_record = logging.makeLogRecord({
+ 'levelno': logging.ERROR,
+ 'levelname': 'ERROR',
+ 'msg': traceback.format_exc(),
+ })
+ job.log(tb_record)
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
if type(e) is JobTimeoutException:
logger.error(e)
diff --git a/netbox/netbox/tests/test_jobs.py b/netbox/netbox/tests/test_jobs.py
index 085047bfc..93bae0e24 100644
--- a/netbox/netbox/tests/test_jobs.py
+++ b/netbox/netbox/tests/test_jobs.py
@@ -83,6 +83,10 @@ class JobRunnerTest(JobRunnerTestCase):
self.assertEqual(job.status, JobStatusChoices.STATUS_ERRORED)
self.assertEqual(job.error, repr(ErroredJobRunner.EXP))
+ self.assertEqual(len(job.log_entries), 1)
+ self.assertEqual(job.log_entries[0]['level'], 'error')
+ self.assertIn('Traceback', job.log_entries[0]['message'])
+ self.assertIn('Test error', job.log_entries[0]['message'])
class EnqueueTest(JobRunnerTestCase):