(feature) Improve run debugging and log filtering #12
@@ -29,6 +29,22 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="window">Time window</label>
|
||||
<select id="window" name="window">
|
||||
{% for value, label in time_windows.items %}
|
||||
<option value="{{ value }}" {% if selected_window == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="host">Host contains</label>
|
||||
<input id="host" name="host" value="{{ host_filter }}" placeholder="web-01.example.test">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="run">Run</label>
|
||||
<input id="run" name="run" value="{{ run_filter }}" inputmode="numeric" placeholder="12">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="q">Message contains</label>
|
||||
<input id="q" name="q" value="{{ query }}">
|
||||
|
||||
@@ -150,21 +150,41 @@ class ViewTests(TestCase):
|
||||
completed = subprocess.CompletedProcess(
|
||||
args=["journalctl"],
|
||||
returncode=0,
|
||||
stdout="2026-05-19 pobsync-worker.service failed backup\n2026-05-19 pobsync-web.service started\n",
|
||||
stdout=(
|
||||
"2026-05-19 pobsync-worker.service web-01 failed backup run 12\n"
|
||||
"2026-05-19 pobsync-worker.service web-02 failed backup run 12\n"
|
||||
"2026-05-19 pobsync-web.service web-01 started run 12\n"
|
||||
),
|
||||
stderr="",
|
||||
)
|
||||
|
||||
with patch("pobsync_backend.views.shutil.which", return_value="/usr/bin/journalctl"), patch(
|
||||
"pobsync_backend.views.subprocess.run", return_value=completed
|
||||
) as run:
|
||||
response = self.client.get(reverse("logs"), {"unit": "pobsync-worker.service", "priority": "0..3", "q": "failed"})
|
||||
response = self.client.get(
|
||||
reverse("logs"),
|
||||
{
|
||||
"unit": "pobsync-worker.service",
|
||||
"priority": "0..3",
|
||||
"window": "6h",
|
||||
"host": "web-01",
|
||||
"run": "12",
|
||||
"q": "failed",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Logs")
|
||||
self.assertContains(response, "failed backup")
|
||||
self.assertContains(response, "web-01 failed backup run 12")
|
||||
self.assertNotContains(response, "web-02 failed backup run 12")
|
||||
self.assertNotContains(response, "started")
|
||||
self.assertIn("-u", run.call_args.args[0])
|
||||
self.assertIn("pobsync-worker.service", run.call_args.args[0])
|
||||
command = run.call_args.args[0]
|
||||
self.assertIn("-u", command)
|
||||
self.assertIn("pobsync-worker.service", command)
|
||||
self.assertIn("-p", command)
|
||||
self.assertIn("0..3", command)
|
||||
self.assertIn("--since", command)
|
||||
self.assertIn("6 hours ago", command)
|
||||
|
||||
def test_ssh_credentials_view_creates_key(self) -> None:
|
||||
self.client.force_login(self.staff_user)
|
||||
|
||||
@@ -703,8 +703,26 @@ def _log_context(request) -> dict[str, object]:
|
||||
"6": "Info",
|
||||
"7": "Debug",
|
||||
}
|
||||
time_windows = {
|
||||
"1h": "Last hour",
|
||||
"6h": "Last 6 hours",
|
||||
"24h": "Last 24 hours",
|
||||
"7d": "Last 7 days",
|
||||
"": "All available",
|
||||
}
|
||||
since_values = {
|
||||
"1h": "1 hour ago",
|
||||
"6h": "6 hours ago",
|
||||
"24h": "24 hours ago",
|
||||
"7d": "7 days ago",
|
||||
}
|
||||
selected_unit = request.GET.get("unit", "")
|
||||
priority = request.GET.get("priority", "0..4")
|
||||
time_window = request.GET.get("window", "24h")
|
||||
if time_window not in time_windows:
|
||||
time_window = "24h"
|
||||
host_filter = request.GET.get("host", "").strip()
|
||||
run_filter = request.GET.get("run", "").strip()
|
||||
query = request.GET.get("q", "").strip()
|
||||
lines = []
|
||||
error = ""
|
||||
@@ -713,6 +731,8 @@ def _log_context(request) -> dict[str, object]:
|
||||
error = "journalctl is not available in this runtime."
|
||||
else:
|
||||
command = ["journalctl", "--no-pager", "-n", "300", "-o", "short-iso"]
|
||||
if time_window:
|
||||
command.extend(["--since", since_values[time_window]])
|
||||
if selected_unit in units:
|
||||
command.extend(["-u", selected_unit])
|
||||
else:
|
||||
@@ -725,16 +745,38 @@ def _log_context(request) -> dict[str, object]:
|
||||
error = result.stderr.strip() or "Could not read journal logs."
|
||||
else:
|
||||
lines = result.stdout.splitlines()
|
||||
if query:
|
||||
lowered_query = query.lower()
|
||||
lines = [line for line in lines if lowered_query in line.lower()]
|
||||
lines = _filter_log_lines(lines, query=query, host=host_filter, run_id=run_filter)
|
||||
|
||||
return {
|
||||
"units": units,
|
||||
"priorities": priorities,
|
||||
"time_windows": time_windows,
|
||||
"selected_unit": selected_unit,
|
||||
"selected_priority": priority,
|
||||
"selected_window": time_window,
|
||||
"host_filter": host_filter,
|
||||
"run_filter": run_filter,
|
||||
"query": query,
|
||||
"lines": lines,
|
||||
"error": error,
|
||||
}
|
||||
|
||||
|
||||
def _filter_log_lines(lines: list[str], *, query: str, host: str, run_id: str) -> list[str]:
|
||||
filters = []
|
||||
if query:
|
||||
filters.append(lambda line: query.lower() in line.lower())
|
||||
if host:
|
||||
filters.append(lambda line: host.lower() in line.lower())
|
||||
if run_id:
|
||||
run_tokens = (
|
||||
f"run {run_id}",
|
||||
f"run={run_id}",
|
||||
f"run_id={run_id}",
|
||||
f"run-{run_id}",
|
||||
f"#{run_id}",
|
||||
)
|
||||
filters.append(lambda line: any(token in line.lower() for token in run_tokens))
|
||||
if not filters:
|
||||
return lines
|
||||
return [line for line in lines if all(matches(line) for matches in filters)]
|
||||
|
||||
Reference in New Issue
Block a user