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..e685cfee8 100644 --- a/netbox/netbox/jobs.py +++ b/netbox/netbox/jobs.py @@ -1,6 +1,9 @@ import logging +import os +import traceback from abc import ABC, abstractmethod from datetime import timedelta +from pathlib import Path from django.core.exceptions import ImproperlyConfigured from django.utils import timezone @@ -21,6 +24,11 @@ __all__ = ( 'system_job', ) +# The installation root, e.g. "/opt/netbox/". Used to strip absolute path +# prefixes from traceback file paths before recording them in the job log. +# jobs.py lives at /netbox/netbox/jobs.py, so parents[2] is the root. +_INSTALL_ROOT = str(Path(__file__).resolve().parents[2]) + os.sep + def system_job(interval): """ @@ -107,6 +115,13 @@ class JobRunner(ABC): job.terminate(status=JobStatusChoices.STATUS_FAILED) except Exception as e: + tb_str = traceback.format_exc().replace(_INSTALL_ROOT, '') + tb_record = logging.makeLogRecord({ + 'levelno': logging.ERROR, + 'levelname': 'ERROR', + 'msg': tb_str, + }) + 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..5bb073951 100644 --- a/netbox/netbox/tests/test_jobs.py +++ b/netbox/netbox/tests/test_jobs.py @@ -10,6 +10,7 @@ from core.models import DataSource, Job from utilities.testing import disable_warnings from ..jobs import * +from ..jobs import _INSTALL_ROOT class TestJobRunner(JobRunner): @@ -83,6 +84,12 @@ 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') + tb_message = job.log_entries[0]['message'] + self.assertIn('Traceback', tb_message) + self.assertIn('Test error', tb_message) + self.assertNotIn(_INSTALL_ROOT, tb_message) class EnqueueTest(JobRunnerTestCase):