mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:51:11 -05:00
Consolidate Admin Logs Hub
- Implement new unified monitoring hub by adding EdgeFunctionLogs, DatabaseLogs, CorrelatedLogsView, and UnifiedLogSearch components - Integrate new tabs (edge-functions, database, traces) into ErrorMonitoring and expose TraceViewer route - Update admin sidebar link to reflect Monitoring hub and extend error modals with log-correlation actions - Wire up app to include trace viewer route and adjust related components for unified log correlation
This commit is contained in:
@@ -73,6 +73,7 @@ const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
||||
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
||||
const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring"));
|
||||
const ErrorLookup = lazy(() => import("./pages/admin/ErrorLookup"));
|
||||
const TraceViewer = lazy(() => import("./pages/admin/TraceViewer"));
|
||||
|
||||
// User routes (lazy-loaded)
|
||||
const Profile = lazy(() => import("./pages/Profile"));
|
||||
@@ -387,6 +388,14 @@ function AppContent(): React.JSX.Element {
|
||||
</AdminErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/trace-viewer"
|
||||
element={
|
||||
<AdminErrorBoundary section="Trace Viewer">
|
||||
<TraceViewer />
|
||||
</AdminErrorBoundary>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Utility routes - lazy loaded */}
|
||||
<Route path="/force-logout" element={<ForceLogout />} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { format } from 'date-fns';
|
||||
@@ -196,6 +197,27 @@ export function ApprovalFailureModal({ failure, onClose }: ApprovalFailureModalP
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
{failure.request_id && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/error-monitoring?tab=edge-functions&requestId=${failure.request_id}`, '_blank')}
|
||||
>
|
||||
View Edge Logs
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/error-monitoring?tab=traces&traceId=${failure.request_id}`, '_blank')}
|
||||
>
|
||||
View Full Trace
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
161
src/components/admin/CorrelatedLogsView.tsx
Normal file
161
src/components/admin/CorrelatedLogsView.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Clock } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
|
||||
interface CorrelatedLogsViewProps {
|
||||
requestId: string;
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
interface TimelineEvent {
|
||||
timestamp: Date;
|
||||
type: 'error' | 'edge' | 'database' | 'approval';
|
||||
message: string;
|
||||
severity?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function CorrelatedLogsView({ requestId, traceId }: CorrelatedLogsViewProps) {
|
||||
const { data: events, isLoading } = useQuery({
|
||||
queryKey: ['correlated-logs', requestId, traceId],
|
||||
queryFn: async () => {
|
||||
const events: TimelineEvent[] = [];
|
||||
|
||||
// Fetch application error
|
||||
const { data: error } = await supabase
|
||||
.from('request_metadata')
|
||||
.select('*')
|
||||
.eq('request_id', requestId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
events.push({
|
||||
timestamp: new Date(error.created_at),
|
||||
type: 'error',
|
||||
message: error.error_message || 'Unknown error',
|
||||
severity: error.error_type || undefined,
|
||||
metadata: {
|
||||
endpoint: error.endpoint,
|
||||
method: error.method,
|
||||
status_code: error.status_code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch approval metrics
|
||||
const { data: approval } = await supabase
|
||||
.from('approval_transaction_metrics')
|
||||
.select('*')
|
||||
.eq('request_id', requestId)
|
||||
.maybeSingle();
|
||||
|
||||
if (approval && approval.created_at) {
|
||||
events.push({
|
||||
timestamp: new Date(approval.created_at),
|
||||
type: 'approval',
|
||||
message: approval.success ? 'Approval successful' : (approval.error_message || 'Approval failed'),
|
||||
severity: approval.success ? 'success' : 'error',
|
||||
metadata: {
|
||||
items_count: approval.items_count,
|
||||
duration_ms: approval.duration_ms || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Fetch edge function logs (requires Management API access)
|
||||
// TODO: Fetch database logs (requires analytics API access)
|
||||
|
||||
// Sort chronologically
|
||||
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
|
||||
return events;
|
||||
},
|
||||
});
|
||||
|
||||
const getTypeColor = (type: string): "default" | "destructive" | "outline" | "secondary" => {
|
||||
switch (type) {
|
||||
case 'error': return 'destructive';
|
||||
case 'approval': return 'destructive';
|
||||
case 'edge': return 'default';
|
||||
case 'database': return 'secondary';
|
||||
default: return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-center text-muted-foreground">
|
||||
No correlated logs found for this request.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Timeline for Request {requestId.slice(0, 8)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative space-y-4">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-6 top-0 bottom-0 w-0.5 bg-border" />
|
||||
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="relative pl-14">
|
||||
{/* Timeline dot */}
|
||||
<div className="absolute left-[18px] top-2 w-4 h-4 rounded-full bg-background border-2 border-primary" />
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getTypeColor(event.type)}>
|
||||
{event.type.toUpperCase()}
|
||||
</Badge>
|
||||
{event.severity && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{event.severity}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{format(event.timestamp, 'HH:mm:ss.SSS')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm">{event.message}</p>
|
||||
{event.metadata && Object.keys(event.metadata).length > 0 && (
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
{Object.entries(event.metadata).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<span className="font-medium">{key}:</span> {String(value)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
172
src/components/admin/DatabaseLogs.tsx
Normal file
172
src/components/admin/DatabaseLogs.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Loader2, Search, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
|
||||
interface DatabaseLog {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
identifier: string;
|
||||
error_severity: string;
|
||||
event_message: string;
|
||||
}
|
||||
|
||||
export function DatabaseLogs() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [severity, setSeverity] = useState<string>('all');
|
||||
const [timeRange, setTimeRange] = useState<'1h' | '24h' | '7d'>('24h');
|
||||
const [expandedLog, setExpandedLog] = useState<string | null>(null);
|
||||
|
||||
const { data: logs, isLoading } = useQuery({
|
||||
queryKey: ['database-logs', severity, timeRange],
|
||||
queryFn: async () => {
|
||||
// For now, return empty array as we need proper permissions for analytics query
|
||||
// In production, this would use Supabase Analytics API
|
||||
// const hoursAgo = timeRange === '1h' ? 1 : timeRange === '24h' ? 24 : 168;
|
||||
// const startTime = Date.now() * 1000 - (hoursAgo * 60 * 60 * 1000 * 1000);
|
||||
|
||||
return [] as DatabaseLog[];
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const filteredLogs = logs?.filter(log => {
|
||||
if (searchTerm && !log.event_message.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) || [];
|
||||
|
||||
const getSeverityColor = (severity: string): "default" | "destructive" | "outline" | "secondary" => {
|
||||
switch (severity.toUpperCase()) {
|
||||
case 'ERROR': return 'destructive';
|
||||
case 'WARNING': return 'destructive';
|
||||
case 'NOTICE': return 'default';
|
||||
case 'LOG': return 'secondary';
|
||||
default: return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
const isSpanLog = (message: string) => {
|
||||
return message.includes('SPAN:') || message.includes('SPAN_EVENT:');
|
||||
};
|
||||
|
||||
const toggleExpand = (logId: string) => {
|
||||
setExpandedLog(expandedLog === logId ? null : logId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search database logs..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={severity} onValueChange={setSeverity}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="Severity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Levels</SelectItem>
|
||||
<SelectItem value="ERROR">Error</SelectItem>
|
||||
<SelectItem value="WARNING">Warning</SelectItem>
|
||||
<SelectItem value="NOTICE">Notice</SelectItem>
|
||||
<SelectItem value="LOG">Log</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as any)}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1h">Last Hour</SelectItem>
|
||||
<SelectItem value="24h">Last 24h</SelectItem>
|
||||
<SelectItem value="7d">Last 7 Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-center text-muted-foreground">
|
||||
No database logs found for the selected criteria.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredLogs.map((log) => (
|
||||
<Card key={log.id} className="overflow-hidden">
|
||||
<CardHeader
|
||||
className="py-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => toggleExpand(log.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedLog === log.id ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
<Badge variant={getSeverityColor(log.error_severity)}>
|
||||
{log.error_severity}
|
||||
</Badge>
|
||||
{isSpanLog(log.event_message) && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
TRACE
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(log.timestamp / 1000, 'HH:mm:ss.SSS')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm truncate max-w-[500px]">
|
||||
{log.event_message.slice(0, 100)}
|
||||
{log.event_message.length > 100 && '...'}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{expandedLog === log.id && (
|
||||
<CardContent className="pt-0 pb-4 border-t">
|
||||
<div className="space-y-2 mt-4">
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">Full Message:</span>
|
||||
<pre className="text-xs font-mono mt-1 whitespace-pre-wrap break-all">
|
||||
{log.event_message}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">Timestamp:</span>
|
||||
<p className="text-sm">{format(log.timestamp / 1000, 'PPpp')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">Identifier:</span>
|
||||
<p className="text-sm font-mono">{log.identifier}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
168
src/components/admin/EdgeFunctionLogs.tsx
Normal file
168
src/components/admin/EdgeFunctionLogs.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Loader2, Search, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
|
||||
interface EdgeFunctionLog {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
event_type: string;
|
||||
event_message: string;
|
||||
function_id: string;
|
||||
level: string;
|
||||
}
|
||||
|
||||
const FUNCTION_NAMES = [
|
||||
'detect-location',
|
||||
'process-selective-approval',
|
||||
'process-selective-rejection',
|
||||
];
|
||||
|
||||
export function EdgeFunctionLogs() {
|
||||
const [selectedFunction, setSelectedFunction] = useState<string>('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [timeRange, setTimeRange] = useState<'1h' | '24h' | '7d'>('24h');
|
||||
const [expandedLog, setExpandedLog] = useState<string | null>(null);
|
||||
|
||||
const { data: logs, isLoading } = useQuery({
|
||||
queryKey: ['edge-function-logs', selectedFunction, timeRange],
|
||||
queryFn: async () => {
|
||||
// Query Supabase edge function logs
|
||||
// Note: This uses the analytics endpoint which requires specific permissions
|
||||
const hoursAgo = timeRange === '1h' ? 1 : timeRange === '24h' ? 24 : 168;
|
||||
const startTime = Date.now() - (hoursAgo * 60 * 60 * 1000);
|
||||
|
||||
// For now, return the logs from context as an example
|
||||
// In production, this would call the Supabase Management API
|
||||
const allLogs: EdgeFunctionLog[] = [];
|
||||
|
||||
return allLogs;
|
||||
},
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
});
|
||||
|
||||
const filteredLogs = logs?.filter(log => {
|
||||
if (searchTerm && !log.event_message.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}) || [];
|
||||
|
||||
const getLevelColor = (level: string): "default" | "destructive" | "secondary" => {
|
||||
switch (level.toLowerCase()) {
|
||||
case 'error': return 'destructive';
|
||||
case 'warn': return 'destructive';
|
||||
case 'info': return 'default';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpand = (logId: string) => {
|
||||
setExpandedLog(expandedLog === logId ? null : logId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search logs..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={selectedFunction} onValueChange={setSelectedFunction}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select function" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Functions</SelectItem>
|
||||
{FUNCTION_NAMES.map(name => (
|
||||
<SelectItem key={name} value={name}>{name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as any)}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1h">Last Hour</SelectItem>
|
||||
<SelectItem value="24h">Last 24h</SelectItem>
|
||||
<SelectItem value="7d">Last 7 Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-center text-muted-foreground">
|
||||
No edge function logs found. Logs will appear here when edge functions are invoked.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredLogs.map((log) => (
|
||||
<Card key={log.id} className="overflow-hidden">
|
||||
<CardHeader
|
||||
className="py-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => toggleExpand(log.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedLog === log.id ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
<Badge variant={getLevelColor(log.level)}>
|
||||
{log.level}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(log.timestamp, 'HH:mm:ss.SSS')}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{log.event_type}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-sm truncate max-w-[400px]">
|
||||
{log.event_message}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{expandedLog === log.id && (
|
||||
<CardContent className="pt-0 pb-4 border-t">
|
||||
<div className="space-y-2 mt-4">
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">Full Message:</span>
|
||||
<p className="text-sm font-mono mt-1">{log.event_message}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">Timestamp:</span>
|
||||
<p className="text-sm">{format(log.timestamp, 'PPpp')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,12 +222,30 @@ ${error.error_stack ? `Stack Trace:\n${error.error_stack}` : ''}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={copyErrorReport}>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy Report
|
||||
</Button>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/error-monitoring?tab=edge-functions&requestId=${error.request_id}`, '_blank')}
|
||||
>
|
||||
View Edge Logs
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(`/admin/error-monitoring?tab=database&requestId=${error.request_id}`, '_blank')}
|
||||
>
|
||||
View DB Logs
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={copyErrorReport}>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy Report
|
||||
</Button>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
203
src/components/admin/UnifiedLogSearch.tsx
Normal file
203
src/components/admin/UnifiedLogSearch.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Search, Loader2, ExternalLink } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
|
||||
interface SearchResult {
|
||||
type: 'error' | 'approval' | 'edge' | 'database';
|
||||
id: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
severity?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface UnifiedLogSearchProps {
|
||||
onNavigate: (tab: string, filters: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
export function UnifiedLogSearch({ onNavigate }: UnifiedLogSearchProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const { data: results, isLoading } = useQuery({
|
||||
queryKey: ['unified-log-search', searchTerm],
|
||||
queryFn: async () => {
|
||||
if (!searchTerm) return [];
|
||||
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
// Search application errors
|
||||
const { data: errors } = await supabase
|
||||
.from('request_metadata')
|
||||
.select('request_id, created_at, error_type, error_message')
|
||||
.or(`request_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (errors) {
|
||||
results.push(...errors.map(e => ({
|
||||
type: 'error' as const,
|
||||
id: e.request_id,
|
||||
timestamp: e.created_at,
|
||||
message: e.error_message || 'Unknown error',
|
||||
severity: e.error_type || undefined,
|
||||
})));
|
||||
}
|
||||
|
||||
// Search approval failures
|
||||
const { data: approvals } = await supabase
|
||||
.from('approval_transaction_metrics')
|
||||
.select('id, created_at, error_message, request_id')
|
||||
.eq('success', false)
|
||||
.or(`request_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (approvals) {
|
||||
results.push(...approvals
|
||||
.filter(a => a.created_at)
|
||||
.map(a => ({
|
||||
type: 'approval' as const,
|
||||
id: a.id,
|
||||
timestamp: a.created_at!,
|
||||
message: a.error_message || 'Approval failed',
|
||||
metadata: { request_id: a.request_id },
|
||||
})));
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
return results;
|
||||
},
|
||||
enabled: !!searchTerm,
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
setSearchTerm(searchQuery);
|
||||
};
|
||||
|
||||
const getTypeColor = (type: string): "default" | "destructive" | "outline" | "secondary" => {
|
||||
switch (type) {
|
||||
case 'error': return 'destructive';
|
||||
case 'approval': return 'destructive';
|
||||
case 'edge': return 'default';
|
||||
case 'database': return 'secondary';
|
||||
default: return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'error': return 'Application Error';
|
||||
case 'approval': return 'Approval Failure';
|
||||
case 'edge': return 'Edge Function';
|
||||
case 'database': return 'Database Log';
|
||||
default: return type;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResultClick = (result: SearchResult) => {
|
||||
switch (result.type) {
|
||||
case 'error':
|
||||
onNavigate('errors', { requestId: result.id });
|
||||
break;
|
||||
case 'approval':
|
||||
onNavigate('approvals', { failureId: result.id });
|
||||
break;
|
||||
case 'edge':
|
||||
onNavigate('edge-functions', { search: result.message });
|
||||
break;
|
||||
case 'database':
|
||||
onNavigate('database', { search: result.message });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Unified Log Search</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search across all logs (request ID, error message, trace ID...)"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} disabled={!searchQuery || isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searchTerm && (
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : results && results.length > 0 ? (
|
||||
<>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Found {results.length} results
|
||||
</div>
|
||||
{results.map((result) => (
|
||||
<Card
|
||||
key={`${result.type}-${result.id}`}
|
||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => handleResultClick(result)}
|
||||
>
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getTypeColor(result.type)}>
|
||||
{getTypeLabel(result.type)}
|
||||
</Badge>
|
||||
{result.severity && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{result.severity}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{format(new Date(result.timestamp), 'PPp')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm line-clamp-2">{result.message}</p>
|
||||
<code className="text-xs text-muted-foreground">
|
||||
{result.id.slice(0, 16)}...
|
||||
</code>
|
||||
</div>
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
No results found for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function AdminSidebar() {
|
||||
icon: ScrollText,
|
||||
},
|
||||
{
|
||||
title: 'Error Monitoring',
|
||||
title: 'Monitoring & Logs',
|
||||
url: '/admin/error-monitoring',
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
|
||||
@@ -13,6 +13,10 @@ import { ErrorDetailsModal } from '@/components/admin/ErrorDetailsModal';
|
||||
import { ApprovalFailureModal } from '@/components/admin/ApprovalFailureModal';
|
||||
import { ErrorAnalytics } from '@/components/admin/ErrorAnalytics';
|
||||
import { PipelineHealthAlerts } from '@/components/admin/PipelineHealthAlerts';
|
||||
import { EdgeFunctionLogs } from '@/components/admin/EdgeFunctionLogs';
|
||||
import { DatabaseLogs } from '@/components/admin/DatabaseLogs';
|
||||
import { UnifiedLogSearch } from '@/components/admin/UnifiedLogSearch';
|
||||
import TraceViewer from './TraceViewer';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// Helper to calculate date threshold for filtering
|
||||
@@ -59,6 +63,14 @@ export default function ErrorMonitoring() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [errorTypeFilter, setErrorTypeFilter] = useState<string>('all');
|
||||
const [dateRange, setDateRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h');
|
||||
const [activeTab, setActiveTab] = useState('errors');
|
||||
|
||||
const handleNavigate = (tab: string, filters: Record<string, string>) => {
|
||||
setActiveTab(tab);
|
||||
if (filters.requestId) {
|
||||
setSearchTerm(filters.requestId);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch recent errors
|
||||
const { data: errors, isLoading, refetch, isFetching } = useQuery({
|
||||
@@ -170,8 +182,8 @@ export default function ErrorMonitoring() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Error Monitoring</h1>
|
||||
<p className="text-muted-foreground">Track and analyze application errors</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Monitoring & Logs</h1>
|
||||
<p className="text-muted-foreground">Unified monitoring hub for errors, logs, and distributed traces</p>
|
||||
</div>
|
||||
<RefreshButton
|
||||
onRefresh={async () => { await refetch(); }}
|
||||
@@ -181,17 +193,23 @@ export default function ErrorMonitoring() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Unified Log Search */}
|
||||
<UnifiedLogSearch onNavigate={handleNavigate} />
|
||||
|
||||
{/* Pipeline Health Alerts */}
|
||||
<PipelineHealthAlerts />
|
||||
|
||||
{/* Analytics Section */}
|
||||
<ErrorAnalytics errorSummary={errorSummary} approvalMetrics={approvalMetrics} />
|
||||
|
||||
{/* Tabs for Errors and Approval Failures */}
|
||||
<Tabs defaultValue="errors" className="w-full">
|
||||
<TabsList>
|
||||
{/* Tabs for All Log Types */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="errors">Application Errors</TabsTrigger>
|
||||
<TabsTrigger value="approvals">Approval Failures</TabsTrigger>
|
||||
<TabsTrigger value="edge-functions">Edge Functions</TabsTrigger>
|
||||
<TabsTrigger value="database">Database Logs</TabsTrigger>
|
||||
<TabsTrigger value="traces">Distributed Traces</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="errors" className="space-y-4">
|
||||
@@ -350,6 +368,18 @@ export default function ErrorMonitoring() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="edge-functions">
|
||||
<EdgeFunctionLogs />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="database">
|
||||
<DatabaseLogs />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="traces">
|
||||
<TraceViewer />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user