diff --git a/src/pobsync_backend/templates/pobsync_backend/logs.html b/src/pobsync_backend/templates/pobsync_backend/logs.html index ba5adae..537fb3d 100644 --- a/src/pobsync_backend/templates/pobsync_backend/logs.html +++ b/src/pobsync_backend/templates/pobsync_backend/logs.html @@ -29,6 +29,22 @@ {% endfor %} +
+ + +
+
+ + +
+
+ + +
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 7670133..6b00d13 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -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) diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index 5d16cd4..98b4969 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -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)]