Reformat script job data (log, output, tests)

This commit is contained in:
Jeremy Stretch 2024-02-06 16:38:17 -05:00
parent f3ef004e04
commit ece5b4635e
6 changed files with 129 additions and 242 deletions

View File

@ -273,10 +273,11 @@ class BaseScript:
pass pass
def __init__(self): def __init__(self):
self._logs = {} self.messages = [] # Primary script log
self._failed = False self.tests = {} # Mapping of logs for test methods
self._current_method = '' self.output = ''
self._output = '' self.failed = False
self._current_method = '' # Tracks the current test method being run (if any)
# Initiate the log # Initiate the log
self.logger = logging.getLogger(f"netbox.scripts.{self.__module__}.{self.__class__.__name__}") self.logger = logging.getLogger(f"netbox.scripts.{self.__module__}.{self.__class__.__name__}")
@ -285,25 +286,15 @@ class BaseScript:
self.request = None self.request = None
# Compile test methods and initialize results skeleton # Compile test methods and initialize results skeleton
self._logs[''] = {
LogLevelChoices.LOG_SUCCESS: 0,
LogLevelChoices.LOG_INFO: 0,
LogLevelChoices.LOG_WARNING: 0,
LogLevelChoices.LOG_FAILURE: 0,
'log': [],
}
test_methods = []
for method in dir(self): for method in dir(self):
if method.startswith('test_') and callable(getattr(self, method)): if method.startswith('test_') and callable(getattr(self, method)):
test_methods.append(method) self.tests[method] = {
self._logs[method] = {
LogLevelChoices.LOG_SUCCESS: 0, LogLevelChoices.LOG_SUCCESS: 0,
LogLevelChoices.LOG_INFO: 0, LogLevelChoices.LOG_INFO: 0,
LogLevelChoices.LOG_WARNING: 0, LogLevelChoices.LOG_WARNING: 0,
LogLevelChoices.LOG_FAILURE: 0, LogLevelChoices.LOG_FAILURE: 0,
'log': [], 'log': [],
} }
self.test_methods = test_methods
def __str__(self): def __str__(self):
return self.name return self.name
@ -448,10 +439,10 @@ class BaseScript:
if level not in LogLevelChoices.values(): if level not in LogLevelChoices.values():
raise ValueError(f"Invalid logging level: {level}") raise ValueError(f"Invalid logging level: {level}")
if message: # A test method is currently active, so log the message using legacy Report logging
if self._current_method:
# Record to the script's log self.tests[self._current_method]['log'].append((
self._logs[self._current_method]['log'].append((
timezone.now().isoformat(), timezone.now().isoformat(),
level, level,
str(obj) if obj else None, str(obj) if obj else None,
@ -459,19 +450,22 @@ class BaseScript:
str(message), str(message),
)) ))
# Increment the event counter for this level
if level in self.tests[self._current_method]:
self.tests[self._current_method][level] += 1
elif message:
# Record to the script's log
self.messages.append(
(level, str(message))
)
# Record to the system log # Record to the system log
if obj: if obj:
message = f"{obj}: {message}" message = f"{obj}: {message}"
self.logger.log(LogLevelChoices.SYSTEM_LEVELS[level], message) self.logger.log(LogLevelChoices.SYSTEM_LEVELS[level], message)
# Increment the event counter for this level if applicable
if level in self._logs[self._current_method]:
self._logs[self._current_method][level] += 1
# For individual test methods, increment the global counter as well
if self._current_method:
self._logs[''][level] += 1
def log_debug(self, message, obj=None): def log_debug(self, message, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_DEBUG) self._log(message, obj, level=LogLevelChoices.LOG_DEBUG)
@ -486,7 +480,7 @@ class BaseScript:
def log_failure(self, message, obj=None): def log_failure(self, message, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_FAILURE) self._log(message, obj, level=LogLevelChoices.LOG_FAILURE)
self._failed = True self.failed = True
# #
# Convenience functions # Convenience functions
@ -528,7 +522,7 @@ class BaseScript:
self.logger.info(f"Running report") self.logger.info(f"Running report")
try: try:
for method_name in self.test_methods: for method_name in self.tests:
self._current_method = method_name self._current_method = method_name
test_method = getattr(self, method_name) test_method = getattr(self, method_name)
test_method() test_method()
@ -606,11 +600,12 @@ def run_script(data, job, request=None, commit=True, **kwargs):
script.request = request script.request = request
def set_job_data(script): def set_job_data(script):
logs = script._logs
job.data = { job.data = {
'logs': logs, 'log': script.messages,
'output': script._output, 'output': script.output,
'tests': script.tests,
} }
return job return job
def _run_script(): def _run_script():
@ -621,7 +616,7 @@ def run_script(data, job, request=None, commit=True, **kwargs):
try: try:
try: try:
with transaction.atomic(): with transaction.atomic():
script._output = script.run(data, commit) script.output = script.run(data, commit)
if not commit: if not commit:
raise AbortTransaction() raise AbortTransaction()
except AbortTransaction: except AbortTransaction:
@ -635,7 +630,7 @@ def run_script(data, job, request=None, commit=True, **kwargs):
if request: if request:
clear_events.send(request) clear_events.send(request)
job = set_job_data(script) job = set_job_data(script)
if script._failed: if script.failed:
logger.warning(f"Script failed") logger.warning(f"Script failed")
job.terminate(status=JobStatusChoices.STATUS_FAILED) job.terminate(status=JobStatusChoices.STATUS_FAILED)
else: else:

View File

@ -1153,32 +1153,28 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, View):
module = job.object module = job.object
script = module.scripts[job.name]() script = module.scripts[job.name]()
legacy_script = False context = {
legacy_report = False 'script': script,
if job.data and ('logs' not in job.data): 'job': job,
if 'log' in job.data: }
legacy_script = True if job.data and 'log' in job.data:
else: # Script
legacy_report = True context['tests'] = job.data.get('tests', {})
elif job.data:
# Legacy Report
context['tests'] = {
name: data for name, data in job.data.items()
if name.startswith('test_')
}
# If this is an HTMX request, return only the result HTML # If this is an HTMX request, return only the result HTML
if request.htmx: if request.htmx:
response = render(request, 'extras/htmx/script_result.html', { response = render(request, 'extras/htmx/script_result.html', context)
'script': script,
'job': job,
'legacy_script': legacy_script,
'legacy_report': legacy_report,
})
if job.completed or not job.started: if job.completed or not job.started:
response.status_code = 286 response.status_code = 286
return response return response
return render(request, 'extras/script_result.html', { return render(request, 'extras/script_result.html', context)
'script': script,
'job': job,
'legacy_script': legacy_script,
'legacy_report': legacy_report,
})
# #

View File

@ -1,81 +0,0 @@
{% load humanize %}
{% load helpers %}
{% load i18n %}
<p>
{% if job.started %}
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
{% elif job.scheduled %}
{% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
{% else %}
{% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
{% endif %}
{% if job.completed %}
{% trans "Duration" %}: <strong>{{ job.duration }}</strong>
{% endif %}
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
</p>
{% if job.completed %}
<div class="card">
<h5 class="card-header">{% trans "Report Methods" %}</h5>
<div class="card-body">
<table class="table table-hover">
{% for method, data in job.data.items %}
<tr>
<td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td>
<td class="text-end report-stats">
<span class="badge bg-success">{{ data.success }}</span>
<span class="badge bg-info">{{ data.info }}</span>
<span class="badge bg-warning">{{ data.warning }}</span>
<span class="badge bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">{% trans "Report Results" %}</h5>
<div class="card-body">
<table class="table table-hover report">
<thead>
<tr class="table-headings">
<th>{% trans "Time" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Object" %}</th>
<th>{% trans "Message" %}</th>
</tr>
</thead>
<tbody>
{% for method, data in job.data.items %}
<tr>
<th colspan="4" style="font-family: monospace">
<a name="{{ method }}"></a>{{ method }}
</th>
</tr>
{% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td>
<td>
<label class="badge bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
</td>
<td>
{% if obj and url %}
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td class="rendered-markdown">{{ message|markdown }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% elif job.started %}
{% include 'extras/inc/result_pending.html' %}
{% endif %}

View File

@ -1,58 +0,0 @@
{% load humanize %}
{% load helpers %}
{% load log_levels %}
{% load i18n %}
<p>
{% if job.started %}
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
{% elif job.scheduled %}
{% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
{% else %}
{% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
{% endif %}
{% if job.completed %}
{% trans "Duration" %}: <strong>{{ job.duration }}</strong>
{% endif %}
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
</p>
{% if job.completed %}
<div class="card mb-3">
<h5 class="card-header">{% trans "Script Log" %}</h5>
<div class="card-body">
<table class="table table-hover panel-body">
<tr>
<th>{% trans "Line" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Message" %}</th>
</tr>
{% for log in job.data.log %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{% log_level log.status %}</td>
<td class="rendered-markdown">{{ log.message|markdown }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-muted">
{% trans "No log output" %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% if execution_time %}
<div class="card-footer text-end text-muted">
<small>{% trans "Exec Time" %}: {{ execution_time|floatformat:3 }} {% trans "seconds" context "Unit of time" %}</small>
</div>
{% endif %}
</div>
<h4>{% trans "Output" %}</h4>
{% if job.data.output %}
<pre class="block">{{ job.data.output }}</pre>
{% else %}
<p class="text-muted">{% trans "None" %}</p>
{% endif %}
{% elif job.started %}
{% include 'extras/inc/result_pending.html' %}
{% endif %}

View File

@ -1,5 +1,6 @@
{% load humanize %} {% load humanize %}
{% load helpers %} {% load helpers %}
{% load log_levels %}
{% load i18n %} {% load i18n %}
<p> <p>
@ -16,13 +17,49 @@
<span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span> <span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
</p> </p>
{% if job.completed %} {% if job.completed %}
<div class="card"> <div class="card mb-3">
<h5 class="card-header">{% trans "Script Methods" %}</h5> <h5 class="card-header">{% trans "Script Log" %}</h5>
<table class="table table-hover"> <table class="table table-hover panel-body">
{% for method, data in job.data.logs.items %}
<tr> <tr>
<td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td> <th>{% trans "Line" %}</th>
<td class="text-end script-stats"> <th>{% trans "Level" %}</th>
<th>{% trans "Message" %}</th>
</tr>
{% for log in job.data.log %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{% log_level log.status %}</td>
<td class="rendered-markdown">{{ log.message|markdown }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center text-muted">
{% trans "No log output" %}
</td>
</tr>
{% endfor %}
</table>
{% if execution_time %}
<div class="card-footer text-end text-muted">
<small>{% trans "Exec Time" %}: {{ execution_time|floatformat:3 }} {% trans "seconds" context "Unit of time" %}</small>
</div>
{% endif %}
</div>
<h4>{% trans "Output" %}</h4>
{% if job.data.output %}
<pre class="block">{{ job.data.output }}</pre>
{% else %}
<p class="text-muted">{% trans "None" %}</p>
{% endif %}
{% if tests %}
<div class="card">
<h5 class="card-header">{% trans "Report Summary" %}</h5>
<table class="table table-hover">
{% for test, data in tests.items %}
<tr>
<td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
<td class="text-end report-stats">
<span class="badge text-bg-success">{{ data.success }}</span> <span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span> <span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span> <span class="badge text-bg-warning">{{ data.warning }}</span>
@ -32,14 +69,11 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
<h2 class="card-header">{% trans "Script Results" %}</h2>
{% for method, data in job.data.logs.items %}
<div class="card"> <div class="card">
<h5 class="card-header"><a name="{{ method }}"></a>{{ method }}</h5> <h5 class="card-header">{% trans "Report Results" %}</h5>
<table class="table table-hover script"> <table class="table table-hover report">
<thead> <thead>
<tr> <tr class="table-headings">
<th>{% trans "Time" %}</th> <th>{% trans "Time" %}</th>
<th>{% trans "Level" %}</th> <th>{% trans "Level" %}</th>
<th>{% trans "Object" %}</th> <th>{% trans "Object" %}</th>
@ -47,6 +81,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for test, data in tests.items %}
<tr>
<th colspan="4" style="font-family: monospace">
<a name="{{ test }}"></a>{{ test }}
</th>
</tr>
{% for time, level, obj, url, message in data.log %} {% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}"> <tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td> <td>{{ time }}</td>
@ -65,10 +105,11 @@
<td class="rendered-markdown">{{ message|markdown }}</td> <td class="rendered-markdown">{{ message|markdown }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endfor %} {% endif %}
{% elif job.started %} {% elif job.started %}
{% include 'extras/inc/result_pending.html' %} {% include 'extras/inc/result_pending.html' %}
{% endif %} {% endif %}

View File

@ -44,13 +44,7 @@
<div role="tabpanel" class="tab-pane active" id="log"> <div role="tabpanel" class="tab-pane active" id="log">
<div class="row"> <div class="row">
<div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}> <div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}>
{% if legacy_script %}
{% include 'extras/htmx/legacy_script_result.html' %}
{% elif legacy_report %}
{% include 'extras/htmx/legacy_report_result.html' %}
{% else %}
{% include 'extras/htmx/script_result.html' %} {% include 'extras/htmx/script_result.html' %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>