feat: Implement advanced access logging with Caddy JSON format, filtering, and download

This commit is contained in:
Wikid82
2025-11-20 13:19:01 -05:00
parent 28c04ff3aa
commit 6db6652cd2
3 changed files with 209 additions and 4 deletions

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { Search, Download, RefreshCw } from 'lucide-react';
import { Button } from './ui/Button';
interface LogFiltersProps {
search: string;
onSearchChange: (value: string) => void;
status: string;
onStatusChange: (value: string) => void;
host: string;
onHostChange: (value: string) => void;
onRefresh: () => void;
onDownload: () => void;
isLoading: boolean;
}
export const LogFilters: React.FC<LogFiltersProps> = ({
search,
onSearchChange,
status,
onStatusChange,
host,
onHostChange,
onRefresh,
onDownload,
isLoading
}) => {
return (
<div className="flex flex-col md:flex-row gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-4 w-4 text-gray-400" />
</div>
<input
type="text"
placeholder="Search logs..."
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="block w-full pl-10 rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="w-full md:w-48">
<input
type="text"
placeholder="Filter by Host"
value={host}
onChange={(e) => onHostChange(e.target.value)}
className="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white"
/>
</div>
<div className="w-full md:w-32">
<select
value={status}
onChange={(e) => onStatusChange(e.target.value)}
className="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white"
>
<option value="">All Status</option>
<option value="2xx">2xx Success</option>
<option value="3xx">3xx Redirect</option>
<option value="4xx">4xx Client Error</option>
<option value="5xx">5xx Server Error</option>
</select>
</div>
<div className="flex gap-2">
<Button onClick={onRefresh} variant="secondary" size="sm" isLoading={isLoading}>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
<Button onClick={onDownload} variant="secondary" size="sm">
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { CaddyAccessLog } from '../api/logs';
import { format } from 'date-fns';
interface LogTableProps {
logs: CaddyAccessLog[];
isLoading: boolean;
}
export const LogTable: React.FC<LogTableProps> = ({ logs, isLoading }) => {
if (isLoading) {
return (
<div className="w-full h-64 flex items-center justify-center text-gray-500">
Loading logs...
</div>
);
}
if (!logs || logs.length === 0) {
return (
<div className="w-full h-64 flex items-center justify-center text-gray-500">
No logs found matching criteria.
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Time</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Method</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Host</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Path</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">IP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Latency</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Message</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{logs.map((log, idx) => (
<tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{format(new Date(log.ts * 1000), 'MMM d HH:mm:ss')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{log.status > 0 && (
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${log.status >= 500 ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' :
log.status >= 400 ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
log.status >= 300 ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'}`}>
{log.status}
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{log.request?.method}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{log.request?.host}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate" title={log.request?.uri}>
{log.request?.uri}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{log.request?.remote_ip}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{log.duration > 0 ? (log.duration * 1000).toFixed(2) + 'ms' : ''}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate" title={log.msg}>
{log.msg}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};