185 lines
7.1 KiB
TypeScript
185 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Search, Clock, AlertCircle, ExternalLink } from 'lucide-react';
|
|
import { api } from '@/lib/api';
|
|
import { TraceTimeline } from '@/components/traces/TraceTimeline';
|
|
import { formatDuration, formatTime } from '@/lib/utils';
|
|
|
|
interface Trace {
|
|
trace_id: string;
|
|
services: string[];
|
|
start_time: string;
|
|
duration_ns: number;
|
|
span_count: number;
|
|
has_error: boolean;
|
|
root_span?: {
|
|
operation: string;
|
|
service: string;
|
|
};
|
|
}
|
|
|
|
export default function TracesPage() {
|
|
const [service, setService] = useState('');
|
|
const [operation, setOperation] = useState('');
|
|
const [minDuration, setMinDuration] = useState('');
|
|
const [onlyErrors, setOnlyErrors] = useState(false);
|
|
const [selectedTrace, setSelectedTrace] = useState<string | null>(null);
|
|
|
|
const { data: traces, isLoading, refetch } = useQuery({
|
|
queryKey: ['traces', service, operation, minDuration, onlyErrors],
|
|
queryFn: () => api.get('/api/v1/traces', {
|
|
service,
|
|
operation,
|
|
min_duration_ms: minDuration,
|
|
error: onlyErrors ? 'true' : '',
|
|
from: new Date(Date.now() - 3600000).toISOString(),
|
|
}),
|
|
});
|
|
|
|
const { data: traceDetail } = useQuery({
|
|
queryKey: ['trace', selectedTrace],
|
|
queryFn: () => api.get(`/api/v1/traces/${selectedTrace}`),
|
|
enabled: !!selectedTrace,
|
|
});
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold text-white">Traces</h1>
|
|
<button
|
|
onClick={() => refetch()}
|
|
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
|
|
>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-4 p-4 bg-gray-900/50 rounded-lg border border-gray-800">
|
|
<div className="flex-1 min-w-[200px]">
|
|
<label className="block text-sm text-gray-400 mb-1">Service</label>
|
|
<input
|
|
type="text"
|
|
value={service}
|
|
onChange={(e) => setService(e.target.value)}
|
|
placeholder="All services"
|
|
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
<div className="flex-1 min-w-[200px]">
|
|
<label className="block text-sm text-gray-400 mb-1">Operation</label>
|
|
<input
|
|
type="text"
|
|
value={operation}
|
|
onChange={(e) => setOperation(e.target.value)}
|
|
placeholder="All operations"
|
|
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
<div className="w-32">
|
|
<label className="block text-sm text-gray-400 mb-1">Min Duration</label>
|
|
<input
|
|
type="text"
|
|
value={minDuration}
|
|
onChange={(e) => setMinDuration(e.target.value)}
|
|
placeholder="ms"
|
|
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<label className="flex items-center gap-2 px-3 py-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={onlyErrors}
|
|
onChange={(e) => setOnlyErrors(e.target.checked)}
|
|
className="rounded border-gray-600 bg-gray-800 text-indigo-500 focus:ring-indigo-500"
|
|
/>
|
|
<span className="text-sm text-gray-300">Errors only</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Trace List */}
|
|
<div className="space-y-2">
|
|
<h2 className="text-lg font-semibold text-white">
|
|
{isLoading ? 'Loading...' : `${traces?.traces?.length ?? 0} traces`}
|
|
</h2>
|
|
<div className="space-y-2 max-h-[600px] overflow-auto">
|
|
{traces?.traces?.map((trace: Trace) => (
|
|
<div
|
|
key={trace.trace_id}
|
|
onClick={() => setSelectedTrace(trace.trace_id)}
|
|
className={`p-4 bg-gray-900/50 rounded-lg border cursor-pointer transition-colors ${
|
|
selectedTrace === trace.trace_id
|
|
? 'border-indigo-500'
|
|
: 'border-gray-800 hover:border-gray-700'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
{trace.has_error && (
|
|
<AlertCircle className="h-4 w-4 text-red-400" />
|
|
)}
|
|
<span className="font-medium text-white">
|
|
{trace.root_span?.operation || trace.trace_id.slice(0, 16)}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{trace.services?.slice(0, 3).map((svc) => (
|
|
<span
|
|
key={svc}
|
|
className="px-2 py-0.5 text-xs bg-gray-800 text-gray-300 rounded"
|
|
>
|
|
{svc}
|
|
</span>
|
|
))}
|
|
{(trace.services?.length ?? 0) > 3 && (
|
|
<span className="text-xs text-gray-500">
|
|
+{trace.services.length - 3} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="text-right text-sm">
|
|
<div className="flex items-center gap-1 text-gray-400">
|
|
<Clock className="h-3 w-3" />
|
|
{formatDuration(trace.duration_ns)}
|
|
</div>
|
|
<div className="text-gray-500">
|
|
{trace.span_count} spans
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-2 text-xs text-gray-500">
|
|
{formatTime(trace.start_time)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!isLoading && (!traces?.traces || traces.traces.length === 0) && (
|
|
<div className="text-center py-8 text-gray-500">
|
|
No traces found
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trace Detail */}
|
|
<div className="space-y-2">
|
|
<h2 className="text-lg font-semibold text-white">Trace Timeline</h2>
|
|
{selectedTrace && traceDetail ? (
|
|
<TraceTimeline trace={traceDetail} />
|
|
) : (
|
|
<div className="flex items-center justify-center h-64 bg-gray-900/50 rounded-lg border border-gray-800 text-gray-500">
|
|
Select a trace to view timeline
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|