import React, { useState, useEffect } from 'react'; import { Search, Clock, Server, ArrowRight, ChevronRight, ChevronDown, Zap, AlertCircle, CheckCircle, XCircle, BarChart3, TrendingUp, RefreshCw, Eye, GitBranch, Code, Box, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import { StatStrip } from '@/components/ui/stat-strip'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; import { toast } from 'sonner'; import { traceApi } from '@/api'; import { PageShell } from '@/components/layout/PageShell'; import { PageLoading } from '@/components/layout/PageLoading'; import { PageEmpty } from '@/components/layout/PageEmpty'; import type { JsonData } from '@/types/common'; interface Trace { id: string; traceId: string; serviceName: string; operationName: string; startTime: string; duration: number; status: 'success' | 'error' | 'pending'; spanCount: number; tags: Record; spans?: Span[]; } interface Span { id: string; spanId: string; parentSpanId?: string; serviceName: string; operationName: string; startTime: string; duration: number; status: 'success' | 'error' | 'pending'; kind: 'client' | 'server' | 'producer' | 'consumer' | 'internal'; tags: Record; logs: SpanLog[]; children: Span[]; } interface SpanLog { timestamp: string; fields: Record; } interface ServiceMetrics { serviceName: string; requestCount: number; errorRate: number; avgDuration: number; p95Duration: number; p99Duration: number; } const TraceManager: React.FC = () => { const [traces, setTraces] = useState([]); const [services, setServices] = useState([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [serviceFilter, setServiceFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [timeRange, setTimeRange] = useState('1h'); const [activeTab, setActiveTab] = useState('traces'); const [selectedTrace, setSelectedTrace] = useState(null); const [selectedSpan, setSelectedSpan] = useState(null); const [traceDetailOpen, setTraceDetailOpen] = useState(false); const [spanDetailOpen, setSpanDetailOpen] = useState(false); useEffect(() => { fetchData(); }, [timeRange]); const fetchData = async () => { await Promise.all([fetchTraces(), fetchServices()]); }; const fetchTraces = async () => { setLoading(true); try { const res = await traceApi.getTraces({ timeRange, service: serviceFilter, status: statusFilter }); setTraces(res.data || []); } catch { toast.error('获取链路数据失败'); } finally { setLoading(false); } }; const fetchServices = async () => { try { const res = await traceApi.getServices(timeRange); setServices(res.data || []); } catch { toast.error('获取服务数据失败'); } }; const fetchTraceDetail = async (traceId: string) => { try { const res = await traceApi.getTraceDetail(traceId); return res.data; } catch { toast.error('获取链路详情失败'); return null; } }; const getStatusBadge = (status: string) => { switch (status) { case 'success': return 成功; case 'error': return 错误; case 'pending': return 进行中; default: return 未知; } }; const getKindIcon = (kind: string) => { switch (kind) { case 'client': return ; case 'server': return ; case 'producer': return ; case 'consumer': return ; default: return ; } }; const getStatusColor = (status: string) => { switch (status) { case 'success': return 'bg-green-500'; case 'error': return 'bg-red-500'; case 'pending': return 'bg-yellow-500'; default: return 'bg-[var(--dg-bg-active)]'; } }; const formatDuration = (ms: number) => { if (ms < 1) return `${(ms * 1000).toFixed(2)}μs`; if (ms < 1000) return `${ms.toFixed(2)}ms`; return `${(ms / 1000).toFixed(2)}s`; }; const filteredTraces = traces.filter(trace => { const matchSearch = trace.traceId.toLowerCase().includes(searchQuery.toLowerCase()) || trace.operationName.toLowerCase().includes(searchQuery.toLowerCase()); const matchService = serviceFilter === 'all' || trace.serviceName === serviceFilter; const matchStatus = statusFilter === 'all' || trace.status === statusFilter; return matchSearch && matchService && matchStatus; }); const serviceNames = Array.from(new Set(traces.map(t => t.serviceName))); // 构建Span树 const buildSpanTree = (spans: Span[]): Span[] => { const spanMap = new Map(); const roots: Span[] = []; spans.forEach(span => { spanMap.set(span.spanId, { ...span, children: [] }); }); spans.forEach(span => { const node = spanMap.get(span.spanId)!; if (span.parentSpanId && spanMap.has(span.parentSpanId)) { const parent = spanMap.get(span.parentSpanId)!; parent.children = parent.children || []; parent.children.push(node); } else { roots.push(node); } }); return roots; }; // 渲染Span树 const SpanTreeNode: React.FC<{ span: Span; level: number; totalDuration: number }> = ({ span, level, totalDuration }) => { const [expanded, setExpanded] = useState(true); const widthPercent = totalDuration > 0 ? (span.duration / totalDuration) * 100 : 0; const _offsetPercent = 0; // Simplified return (
{ setSelectedSpan(span); setSpanDetailOpen(true); }} > {span.children?.length > 0 && ( )} {!span.children?.length && }
{getKindIcon(span.kind)} {span.operationName} {span.serviceName}
{formatDuration(span.duration)} {span.status === 'error' && }
{expanded && span.children?.map(child => ( ))}
); }; const pageTabs = [ { id: 'traces', label: '链路追踪' }, { id: 'services', label: '服务拓扑' }, { id: 'metrics', label: '性能指标' }, ]; const tracesToolbar = activeTab === 'traces' ? (
setSearchQuery(e.target.value)} className="pl-10" />
) : undefined; return ( } tabs={pageTabs} activeTab={activeTab} onTabChange={setActiveTab} toolbar={tracesToolbar} metrics={( sum + s.requestCount, 0).toLocaleString(), color: 'blue' }, { label: '错误率', value: `${(services.reduce((sum, s) => sum + s.errorRate, 0) / (services.length || 1)).toFixed(2)}%`, color: 'red' }, { label: '平均延迟', value: formatDuration(services.reduce((sum, s) => sum + s.avgDuration, 0) / (services.length || 1)), color: 'amber' }, { label: '服务数', value: services.length, color: 'purple' }, ]} /> )} actions={activeTab === 'traces' ? undefined : ( )} > {activeTab === 'traces' && (
{loading ? ( ) : filteredTraces.length === 0 ? ( ) : ( filteredTraces.map((trace) => ( )) )}
Trace ID 服务 / 操作 状态 耗时 Span数 时间 操作
{trace.traceId.slice(0, 16)}...
{trace.operationName}
{trace.serviceName}
{getStatusBadge(trace.status)} {formatDuration(trace.duration)} {trace.spanCount} {trace.startTime}
)} {activeTab === 'services' && ( 服务性能概览
{services.map((service) => ( ))}
服务名称 请求数 错误率 平均延迟 P95延迟 P99延迟
{service.serviceName} {service.requestCount.toLocaleString()} 1 ? 'text-red-500' : 'dg-text-green'}> {service.errorRate.toFixed(2)}% {formatDuration(service.avgDuration)} {formatDuration(service.p95Duration)} {formatDuration(service.p99Duration)}
)} {activeTab === 'metrics' && (
延迟分布 延迟分布图表(集成图表库后显示) 错误率趋势 错误率趋势图表(集成图表库后显示)
)} {/* Trace详情对话框 */} Trace详情 {selectedTrace?.traceId} 描述 {selectedTrace && (

{selectedTrace.serviceName}

{selectedTrace.operationName}

{formatDuration(selectedTrace.duration)}

{getStatusBadge(selectedTrace.status)}

Span名称 时间线 耗时
{/* Span树渲染 */} {(selectedTrace as JsonData).spans && buildSpanTree((selectedTrace as JsonData).spans).map(span => ( ))}
)}
{/* Span详情对话框 */} Span详情 描述 {selectedSpan && (

{selectedSpan.spanId}

{selectedSpan.kind}

{selectedSpan.serviceName}

{selectedSpan.operationName}

{getStatusBadge(selectedSpan.status)}

{formatDuration(selectedSpan.duration)}

{Object.keys(selectedSpan.tags).length > 0 && (
{Object.entries(selectedSpan.tags).map(([k, v]) => (
{k} {v}
))}
)} {selectedSpan.logs?.length > 0 && (
{selectedSpan.logs.map((log, idx) => (
{log.timestamp}
{JSON.stringify(log.fields, null, 2)}
))}
)}
)}
); }; export default TraceManager;