mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 02:54:37 +00:00
copy button
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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<string | null>(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({
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className='max-h-[90vh] w-[95vw] max-w-[95vw] overflow-hidden'>
|
||||
<DialogHeader>
|
||||
<div className='flex items-center justify-between'>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<Badge variant='outline' className={getLevelColor(log.levelname)}>
|
||||
{log.levelname}
|
||||
</Badge>
|
||||
<span>Log Entry Details</span>
|
||||
</DialogTitle>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => copyToClipboard(JSON.stringify(log, null, 2))}
|
||||
className='h-8 w-8 p-0'
|
||||
>
|
||||
<Copy className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<Badge variant='outline' className={getLevelColor(log.levelname)}>
|
||||
{log.levelname}
|
||||
</Badge>
|
||||
<span>Log Entry Details</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{log.asctime} • {log.name} • {log.pathname}:{log.lineno}
|
||||
</DialogDescription>
|
||||
@@ -99,8 +96,8 @@ export function LogDetailsDialog({
|
||||
<div className='space-y-6'>
|
||||
<div>
|
||||
<h4 className='mb-2 text-sm font-medium'>Message</h4>
|
||||
<div className='bg-muted rounded-md p-3'>
|
||||
<pre className='font-mono text-sm whitespace-pre-wrap'>
|
||||
<div className='bg-muted max-h-48 overflow-auto rounded-md p-3'>
|
||||
<pre className='font-mono text-sm whitespace-pre break-all'>
|
||||
{log.message}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -111,13 +108,35 @@ export function LogDetailsDialog({
|
||||
<div className='grid grid-cols-1 gap-3'>
|
||||
{standardFields.map((field) => (
|
||||
<div key={field} className='flex flex-col space-y-1'>
|
||||
<span className='text-muted-foreground text-xs font-medium uppercase'>
|
||||
{field}
|
||||
</span>
|
||||
<div className='bg-muted max-h-48 overflow-auto rounded p-2 font-mono text-sm'>
|
||||
<div className='inline-block min-w-full whitespace-nowrap'>
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<span className='text-muted-foreground text-xs font-medium uppercase'>
|
||||
{field}
|
||||
</span>
|
||||
{field === 'request_id' && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => copyToClipboard(String(log[field as keyof LogEntry] || ''), field)}
|
||||
className='h-6 flex-shrink-0 px-2'
|
||||
>
|
||||
{copiedField === field ? (
|
||||
<>
|
||||
<Check className='mr-1 h-3 w-3' />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className='mr-1 h-3 w-3' />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='bg-muted max-h-32 overflow-auto rounded p-2'>
|
||||
<pre className='font-mono text-sm break-all whitespace-pre-wrap'>
|
||||
{String(log[field as keyof LogEntry] || 'N/A')}
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -130,18 +149,18 @@ export function LogDetailsDialog({
|
||||
<div className='grid grid-cols-1 gap-3'>
|
||||
{extraFields.map((field) => (
|
||||
<div key={field} className='flex flex-col space-y-1'>
|
||||
<span className='text-muted-foreground text-xs font-medium uppercase'>
|
||||
<span className='text-muted-foreground truncate text-xs font-medium uppercase'>
|
||||
{field}
|
||||
</span>
|
||||
<div className='bg-muted max-h-48 overflow-auto rounded p-2 font-mono text-sm'>
|
||||
<div className='bg-muted max-h-48 overflow-auto rounded p-2'>
|
||||
{typeof log[field] === 'object' ? (
|
||||
<pre className='inline-block min-w-full text-xs whitespace-pre'>
|
||||
<pre className='font-mono text-xs break-all whitespace-pre-wrap'>
|
||||
{JSON.stringify(log[field], null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<div className='inline-block min-w-full whitespace-nowrap'>
|
||||
<pre className='font-mono text-sm break-all whitespace-pre-wrap'>
|
||||
{String(log[field] || 'N/A')}
|
||||
</div>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,17 +171,8 @@ export function LogDetailsDialog({
|
||||
|
||||
<div>
|
||||
<h4 className='mb-3 text-sm font-medium'>Raw JSON</h4>
|
||||
<div className='relative'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => copyToClipboard(JSON.stringify(log, null, 2))}
|
||||
className='absolute top-2 right-2 h-8 px-2'
|
||||
>
|
||||
<Copy className='mr-1 h-3 w-3' />
|
||||
Copy
|
||||
</Button>
|
||||
<pre className='bg-muted inline-block max-h-64 min-w-full overflow-auto rounded-md p-4 text-xs whitespace-pre'>
|
||||
<div className='bg-muted max-h-64 overflow-auto rounded-md p-4'>
|
||||
<pre className='text-xs break-all whitespace-pre-wrap'>
|
||||
{JSON.stringify(log, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user