mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 00:31:13 -05:00
Implement distributed tracing across edge functions: - Introduce span types and utilities (logger.ts enhancements) - Replace request tracking with span-based tracing in approval and rejection flows - Propagate and manage W3C trace context in edge tracking - Add span visualization scaffolding (spanVisualizer.ts) and admin TraceViewer UI (TraceViewer.tsx) - Create tracing-related type definitions and support files - Prepare RPC call logging with span context and retries
151 lines
3.7 KiB
TypeScript
151 lines
3.7 KiB
TypeScript
/**
|
|
* 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<string, Span>();
|
|
const childrenMap = new Map<string, Span[]>();
|
|
|
|
// 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<string, unknown>;
|
|
}> {
|
|
const events: Array<{
|
|
spanName: string;
|
|
eventName: string;
|
|
timestamp: number;
|
|
attributes?: Record<string, unknown>;
|
|
}> = [];
|
|
|
|
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);
|
|
}
|