From 23ff99d41f3b8d25b90f5a0b8591e32f0c50bf4e Mon Sep 17 00:00:00 2001 From: 9qeklajc <9qeklajc> Date: Sat, 15 Nov 2025 11:53:27 +0100 Subject: [PATCH] copy button --- routstr/search/log_search.py | 31 +--------- ui/app/logs/log-details-dialog.tsx | 94 +++++++++++++++++------------- 2 files changed, 53 insertions(+), 72 deletions(-) diff --git a/routstr/search/log_search.py b/routstr/search/log_search.py index 6740d3a5..ff69ef3a 100644 --- a/routstr/search/log_search.py +++ b/routstr/search/log_search.py @@ -1,11 +1,3 @@ -""" -Log search functionality. - -This module contains the search logic for filtering log entries. -It can be replaced with more advanced search mechanisms in the future -(e.g., Elasticsearch, full-text search databases, etc.) -""" - import json from pathlib import Path from typing import Any @@ -21,14 +13,6 @@ def search_logs( ) -> list[dict[str, Any]]: """ Search through log files and return matching entries. - - This is a simple file-based search implementation. For better performance - with large log volumes, consider using: - - Elasticsearch - - Splunk - - Loki - - Or other log aggregation/search tools - Args: logs_dir: Path to the logs directory date: Filter by specific date (YYYY-MM-DD format) @@ -40,29 +24,25 @@ def search_logs( Returns: List of log entries matching the criteria """ - log_entries = [] + log_entries: list[dict[str, Any]] = [] if not logs_dir.exists(): return log_entries - # Determine which log files to search log_files = [] if date: log_file = logs_dir / f"app_{date}.log" if log_file.exists(): log_files.append(log_file) else: - # Search last 7 days of logs log_files = sorted( logs_dir.glob("app_*.log"), key=lambda x: x.stat().st_mtime, reverse=True, )[:7] - # Normalize search text for case-insensitive search search_text_lower = search_text.lower() if search_text else None - # Search through log files for log_file in log_files: try: with open(log_file, "r") as f: @@ -70,7 +50,6 @@ def search_logs( try: log_data = json.loads(line.strip()) - # Apply filters if not _matches_filters( log_data, level, request_id, search_text_lower ): @@ -78,23 +57,18 @@ def search_logs( log_entries.append(log_data) - # Stop if we've reached the limit if len(log_entries) >= limit: break except json.JSONDecodeError: - # Skip malformed JSON lines continue - # Stop searching more files if we've reached the limit if len(log_entries) >= limit: break except Exception: - # Skip files that can't be read continue - # Sort by timestamp (most recent first) log_entries.sort(key=lambda x: x.get("asctime", ""), reverse=True) return log_entries @@ -118,15 +92,12 @@ def _matches_filters( Returns: True if the log entry matches all filters, False otherwise """ - # Filter by log level if level and log_data.get("levelname", "").upper() != level.upper(): return False - # Filter by request ID (exact match) if request_id and log_data.get("request_id") != request_id: return False - # Filter by search text (case-insensitive search in message and name) if search_text_lower: message = str(log_data.get("message", "")).lower() name = str(log_data.get("name", "")).lower() diff --git a/ui/app/logs/log-details-dialog.tsx b/ui/app/logs/log-details-dialog.tsx index 842549d7..5df1e7dd 100644 --- a/ui/app/logs/log-details-dialog.tsx +++ b/ui/app/logs/log-details-dialog.tsx @@ -8,7 +8,8 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { Copy } from 'lucide-react'; +import { Copy, Check } from 'lucide-react'; +import { useState } from 'react'; interface LogEntry { asctime: string; @@ -51,10 +52,16 @@ export function LogDetailsDialog({ isOpen, onClose, }: LogDetailsDialogProps) { + const [copiedField, setCopiedField] = useState(null); + if (!log) return null; - const copyToClipboard = (text: string) => { + const copyToClipboard = (text: string, fieldName?: string) => { navigator.clipboard.writeText(text); + if (fieldName) { + setCopiedField(fieldName); + setTimeout(() => setCopiedField(null), 2000); + } }; const allFields = Object.keys(log).filter((key) => key !== 'key'); @@ -74,22 +81,12 @@ export function LogDetailsDialog({ -
- - - {log.levelname} - - Log Entry Details - - -
+ + + {log.levelname} + + Log Entry Details + {log.asctime} • {log.name} • {log.pathname}:{log.lineno} @@ -99,8 +96,8 @@ export function LogDetailsDialog({

Message

-
-
+              
+
                   {log.message}
                 
@@ -111,13 +108,35 @@ export function LogDetailsDialog({
{standardFields.map((field) => (
- - {field} - -
-
+
+ + {field} + + {field === 'request_id' && ( + + )} +
+
+
                         {String(log[field as keyof LogEntry] || 'N/A')}
-                      
+
))} @@ -130,18 +149,18 @@ export function LogDetailsDialog({
{extraFields.map((field) => (
- + {field} -
+
{typeof log[field] === 'object' ? ( -
+                          
                             {JSON.stringify(log[field], null, 2)}
                           
) : ( -
+
                             {String(log[field] || 'N/A')}
-                          
+
)}
@@ -152,17 +171,8 @@ export function LogDetailsDialog({

Raw JSON

-
- -
+              
+
                   {JSON.stringify(log, null, 2)}