/** * Span Visualizer * Reconstructs span trees from logs for debugging distributed traces */ import type { Span } from '@/types/tracing'; export interface SpanTree { span: Span; children: SpanTree[]; totalDuration: number; selfDuration: number; } /** * Build span tree from flat span logs */ export function buildSpanTree(spans: Span[]): SpanTree | null { const spanMap = new Map(); const childrenMap = new Map(); // Index spans for (const span of spans) { spanMap.set(span.spanId, span); if (span.parentSpanId) { if (!childrenMap.has(span.parentSpanId)) { childrenMap.set(span.parentSpanId, []); } childrenMap.get(span.parentSpanId)!.push(span); } } // Find root span const rootSpan = spans.find(s => !s.parentSpanId); if (!rootSpan) return null; // Build tree recursively function buildTree(span: Span): SpanTree { const children = childrenMap.get(span.spanId) || []; const childTrees = children.map(buildTree); const totalDuration = span.duration || 0; const childrenDuration = childTrees.reduce((sum, child) => sum + child.totalDuration, 0); const selfDuration = totalDuration - childrenDuration; return { span, children: childTrees, totalDuration, selfDuration, }; } return buildTree(rootSpan); } /** * Format span tree as ASCII art */ export function formatSpanTree(tree: SpanTree, indent: number = 0): string { const prefix = ' '.repeat(indent); const status = tree.span.status === 'error' ? '❌' : tree.span.status === 'ok' ? '✅' : '⏳'; const line = `${prefix}${status} ${tree.span.name} (${tree.span.duration}ms / self: ${tree.selfDuration}ms)`; const childLines = tree.children.map(child => formatSpanTree(child, indent + 1)); return [line, ...childLines].join('\n'); } /** * Calculate span statistics for a tree */ export function calculateSpanStats(tree: SpanTree): { totalSpans: number; errorCount: number; maxDepth: number; totalDuration: number; criticalPath: string[]; } { let totalSpans = 0; let errorCount = 0; let maxDepth = 0; function traverse(node: SpanTree, depth: number) { totalSpans++; if (node.span.status === 'error') errorCount++; maxDepth = Math.max(maxDepth, depth); node.children.forEach(child => traverse(child, depth + 1)); } traverse(tree, 0); // Find critical path (longest duration path) function findCriticalPath(node: SpanTree): string[] { if (node.children.length === 0) { return [node.span.name]; } const longestChild = node.children.reduce((longest, child) => child.totalDuration > longest.totalDuration ? child : longest ); return [node.span.name, ...findCriticalPath(longestChild)]; } return { totalSpans, errorCount, maxDepth, totalDuration: tree.totalDuration, criticalPath: findCriticalPath(tree), }; } /** * Extract all events from a span tree */ export function extractAllEvents(tree: SpanTree): Array<{ spanName: string; eventName: string; timestamp: number; attributes?: Record; }> { const events: Array<{ spanName: string; eventName: string; timestamp: number; attributes?: Record; }> = []; function traverse(node: SpanTree) { node.span.events.forEach(event => { events.push({ spanName: node.span.name, eventName: event.name, timestamp: event.timestamp, attributes: event.attributes, }); }); node.children.forEach(child => traverse(child)); } traverse(tree); // Sort by timestamp return events.sort((a, b) => a.timestamp - b.timestamp); }