From 6be7398b42629b10ab1c44ef1c80663f4d1fb62a Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 29 Apr 2026 22:40:08 +0100 Subject: [PATCH] feat: Enhance logs view with level filtering and improved entry display Co-authored-by: Copilot --- rad/views.py | 112 ++++++++++++++++++++++++++++++++----- templates/logs_viewer.html | 86 +++++++++++++++++++++++++--- 2 files changed, 174 insertions(+), 24 deletions(-) diff --git a/rad/views.py b/rad/views.py index 6b1bd456..1493ab87 100644 --- a/rad/views.py +++ b/rad/views.py @@ -1,6 +1,7 @@ import secrets from typing import Any, Optional import os +import re from django.conf import settings from django_tables2 import SingleTableMixin @@ -246,32 +247,111 @@ def logs_view(request): Supports ?all=1 to show all entries without pagination limit. """ log_file_path = os.path.join(settings.BASE_DIR, "log.txt") - entries = [] search_query = request.GET.get("q", "").strip() + level_filter = request.GET.get("level", "").strip().upper() show_all = request.GET.get("all") == "1" - + + log_header_re = re.compile( + r"^\[(?P[^\]]+)\]\s+(?P[A-Z]+)\s+\[(?P[^:\]]+):(?P\d+)\]\s+(?P.*)$" + ) + + parsed_entries: list[dict[str, Any]] = [] if os.path.exists(log_file_path): try: with open(log_file_path, "r", encoding="utf-8", errors="ignore") as f: lines = f.readlines() - - for line in reversed(lines): - line = line.rstrip() - if not line: + + current_entry = None + for raw_line in lines: + line = raw_line.rstrip("\n") + if not line.strip(): + if current_entry is not None: + current_entry["details"].append("") continue - - if search_query and search_query.lower() not in line.lower(): + + header_match = log_header_re.match(line) + if header_match: + if current_entry is not None: + parsed_entries.append(current_entry) + + current_entry = { + "timestamp": header_match.group("timestamp"), + "level": header_match.group("level"), + "logger": header_match.group("logger"), + "source_line": header_match.group("line"), + "message": header_match.group("message"), + "details": [], + } continue - - entries.append(line) + + if current_entry is not None: + current_entry["details"].append(line) + else: + parsed_entries.append( + { + "timestamp": "", + "level": "RAW", + "logger": "", + "source_line": "", + "message": line, + "details": [], + } + ) + + if current_entry is not None: + parsed_entries.append(current_entry) + except Exception as e: - entries = [f"Error reading log file: {str(e)}"] + parsed_entries = [ + { + "timestamp": "", + "level": "ERROR", + "logger": "logs_view", + "source_line": "", + "message": f"Error reading log file: {str(e)}", + "details": [], + } + ] else: - entries = [f"Log file not found at {log_file_path}"] - - total = len(entries) + parsed_entries = [ + { + "timestamp": "", + "level": "ERROR", + "logger": "logs_view", + "source_line": "", + "message": f"Log file not found at {log_file_path}", + "details": [], + } + ] + + # Most recent first. + parsed_entries = list(reversed(parsed_entries)) + + available_levels = sorted({entry["level"] for entry in parsed_entries if entry["level"]}) + + filtered_entries = [] + search_lower = search_query.lower() + for entry in parsed_entries: + if level_filter and entry["level"] != level_filter: + continue + + haystack = "\n".join( + [ + entry.get("timestamp", ""), + entry.get("level", ""), + entry.get("logger", ""), + entry.get("message", ""), + "\n".join(entry.get("details", [])), + ] + ).lower() + if search_lower and search_lower not in haystack: + continue + + filtered_entries.append(entry) + + total = len(filtered_entries) limit = None if show_all else 500 - displayed_entries = entries[:limit] if limit else entries + displayed_entries = filtered_entries[:limit] if limit else filtered_entries return render( request, @@ -280,6 +360,8 @@ def logs_view(request): "entries": displayed_entries, "total_entries": total, "search_query": search_query, + "level_filter": level_filter, + "available_levels": available_levels, "log_file_path": log_file_path, "show_all": show_all, "limit": limit, diff --git a/templates/logs_viewer.html b/templates/logs_viewer.html index 50ac74cb..7f093bdf 100644 --- a/templates/logs_viewer.html +++ b/templates/logs_viewer.html @@ -1,5 +1,34 @@ {% extends 'base.html' %} +{% block css %} + {{ block.super }} + +{% endblock %} + {% block content %}
@@ -14,7 +43,7 @@ {% if show_all %} Viewing all {{ total_entries }} entries {% else %} - Showing {{ entries|length }}/{{ total_entries }} entries + Showing {{ entries|length }}/{{ total_entries }} entries {% endif %}
@@ -23,24 +52,32 @@
-
+
+
+ +
- {% if search_query %} + {% if search_query or level_filter %} Clear {% endif %}
{% if show_all %} - Show recent only + Show recent only {% else %} - View all logs + View all logs {% endif %}
@@ -49,10 +86,41 @@ {% if entries %}
-
-
{% for entry in entries %}
-{{ entry }}
+                
+ {% for entry in entries %} +
+
+ {% if entry.level == 'ERROR' or entry.level == 'CRITICAL' %} + {{ entry.level }} + {% elif entry.level == 'WARNING' or entry.level == 'WARN' %} + {{ entry.level }} + {% elif entry.level == 'DEBUG' %} + {{ entry.level }} + {% elif entry.level == 'INFO' %} + {{ entry.level }} + {% else %} + {{ entry.level|default:'LOG' }} + {% endif %} + {% if entry.timestamp %} + {{ entry.timestamp }} + {% endif %} + {% if entry.logger %} + {{ entry.logger }}{% if entry.source_line %}:{{ entry.source_line }}{% endif %} + {% endif %} +
+ +
{{ entry.message }}
+ + {% if entry.details %} +
+ Traceback / Details ({{ entry.details|length }} lines) +
{% for detail_line in entry.details %}
+{{ detail_line }}
 {% endfor %}
+
+ {% endif %} +
+ {% endfor %}
@@ -63,7 +131,7 @@ {% else %} Displaying most recent 500 entries (newest first). {% if total_entries > 500 %} - View all {{ total_entries }} entries. + View all {{ total_entries }} entries. {% endif %} {% endif %} {% if search_query %}