UNPKG

mcp-ai-agent-guidelines

Version:

A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices

445 lines 15.6 kB
/** * Trace Logger for A2A Orchestration * * Provides structured tracing and observability for tool chains: * - Correlation ID propagation * - Execution timeline visualization * - Performance metrics * - Error tracking * - Distributed tracing support */ import { logger } from "./logger.js"; /** * Trace Logger class for managing traces */ export class TraceLogger { events = []; spans = new Map(); activeSpans = new Map(); // correlationId -> spanId /** * Start a new chain trace * * @param context - A2A context */ startChain(context) { this.addEvent({ type: "chain_start", timestamp: new Date(), correlationId: context.correlationId, depth: context.depth, data: { maxDepth: context.maxDepth, chainTimeoutMs: context.chainTimeoutMs, }, }); logger.info("Chain execution started", { correlationId: context.correlationId, maxDepth: context.maxDepth, }); } /** * End a chain trace * * @param context - A2A context * @param success - Whether chain succeeded * @param error - Error message if failed */ endChain(context, success, error) { const totalDurationMs = Date.now() - context.chainStartTime.getTime(); this.addEvent({ type: "chain_end", timestamp: new Date(), correlationId: context.correlationId, depth: context.depth, data: { success, error, totalDurationMs, toolCount: context.executionLog.length, }, }); logger.info("Chain execution ended", { correlationId: context.correlationId, success, totalDurationMs, toolCount: context.executionLog.length, error, }); } /** * Start a tool span * * @param context - A2A context * @param toolName - Tool name * @param inputHash - Input hash * @returns Span ID */ startToolSpan(context, toolName, inputHash) { // Periodically clean up old spans (10% chance) if (Math.random() < 0.1) { this.cleanupOldSpans(); } const spanId = this.generateSpanId(); const parentSpanId = this.activeSpans.get(context.correlationId); const span = { spanId, parentSpanId, correlationId: context.correlationId, toolName, startTime: new Date(), depth: context.depth, status: "pending", inputHash, }; this.spans.set(spanId, span); this.activeSpans.set(context.correlationId, spanId); this.addEvent({ type: "tool_start", timestamp: new Date(), correlationId: context.correlationId, toolName, depth: context.depth, data: { spanId, parentSpanId }, }); return spanId; } /** * End a tool span * * @param spanId - Span ID * @param success - Whether tool succeeded * @param outputSummary - Output summary * @param error - Error message if failed */ endToolSpan(spanId, success, outputSummary, error) { const span = this.spans.get(spanId); if (!span) { logger.warn(`Span ${spanId} not found`); return; } const endTime = new Date(); const durationMs = endTime.getTime() - span.startTime.getTime(); span.endTime = endTime; span.durationMs = durationMs; span.status = success ? "success" : "error"; span.outputSummary = outputSummary; span.error = error; this.addEvent({ type: success ? "tool_end" : "tool_error", timestamp: endTime, correlationId: span.correlationId, toolName: span.toolName, depth: span.depth, data: { spanId, durationMs, success, error, }, }); // Remove from active spans if this was the active span if (this.activeSpans.get(span.correlationId) === spanId) { if (span.parentSpanId) { this.activeSpans.set(span.correlationId, span.parentSpanId); } else { this.activeSpans.delete(span.correlationId); } } } /** * Get all spans for a correlation ID * * @param correlationId - Correlation ID * @returns Array of spans */ getSpans(correlationId) { return Array.from(this.spans.values()).filter((span) => span.correlationId === correlationId); } /** * Get all events for a correlation ID * * @param correlationId - Correlation ID * @returns Array of events */ getEvents(correlationId) { return this.events.filter((event) => event.correlationId === correlationId); } /** * Get execution timeline for a correlation ID * * @param correlationId - Correlation ID * @returns Timeline visualization data */ getTimeline(correlationId) { const spans = this.getSpans(correlationId); if (spans.length === 0) { return { spans: [], totalDurationMs: 0, criticalPath: [] }; } // Calculate total duration const startTimes = spans.map((s) => s.startTime.getTime()); const endTimes = spans .filter((s) => s.endTime) .map((s) => s.endTime?.getTime() ?? 0); const totalDurationMs = endTimes.length > 0 ? Math.max(...endTimes) - Math.min(...startTimes) : 0; // Find critical path (longest chain of dependent spans) const criticalPath = this.findCriticalPath(spans); return { spans, totalDurationMs, criticalPath }; } /** * Export trace data for external systems * * @param correlationId - Correlation ID * @param format - Export format * @returns Formatted trace data */ exportTrace(correlationId, format = "json") { const spans = this.getSpans(correlationId); const events = this.getEvents(correlationId); if (format === "json") { return JSON.stringify({ correlationId, spans, events, summary: { totalSpans: spans.length, successfulSpans: spans.filter((s) => s.status === "success").length, failedSpans: spans.filter((s) => s.status === "error").length, }, }, null, 2); } // OTLP format (simplified) // In production, use proper OTLP library like: // - @opentelemetry/otlp-exporter-base // - @opentelemetry/exporter-trace-otlp-http // See: https://opentelemetry.io/docs/specs/otlp/ return JSON.stringify({ resourceSpans: [ { resource: { attributes: [ { key: "service.name", value: { stringValue: "a2a-orchestrator" }, }, ], }, scopeSpans: [ { spans: spans.map((span) => ({ traceId: correlationId, spanId: span.spanId, parentSpanId: span.parentSpanId, name: span.toolName, startTimeUnixNano: span.startTime.getTime() * 1000000, endTimeUnixNano: span.endTime ? span.endTime.getTime() * 1000000 : undefined, status: { code: span.status === "success" ? 1 : 2, message: span.error, }, attributes: [ { key: "tool.name", value: { stringValue: span.toolName } }, { key: "depth", value: { intValue: span.depth } }, { key: "input.hash", value: { stringValue: span.inputHash }, }, ], })), }, ], }, ], }); } /** * Clear all traces (for testing) */ clear() { this.events = []; this.spans.clear(); this.activeSpans.clear(); } /** * Get summary statistics */ getSummary() { const correlationIds = new Set(this.events.map((e) => e.correlationId)); return { totalChains: correlationIds.size, totalSpans: this.spans.size, totalEvents: this.events.length, avgSpansPerChain: correlationIds.size > 0 ? this.spans.size / correlationIds.size : 0, }; } /** * Maximum number of events to keep in memory */ static MAX_EVENTS = 1000; /** * Maximum number of spans to keep in memory per correlation ID */ static MAX_SPANS_PER_CORRELATION = 100; /** * Maximum age of spans to keep (in milliseconds) */ static MAX_SPAN_AGE_MS = 3600000; // 1 hour /** * Add a trace event */ addEvent(event) { this.events.push(event); // Keep only last MAX_EVENTS to prevent memory leaks if (this.events.length > TraceLogger.MAX_EVENTS) { this.events = this.events.slice(-TraceLogger.MAX_EVENTS); } } /** * Clean up old spans to prevent memory leaks in long-running servers */ cleanupOldSpans() { const now = Date.now(); const spanIdsToRemove = []; // Remove old spans based on age for (const [spanId, span] of this.spans.entries()) { // Check if the span is too old if (span.endTime && now - span.endTime.getTime() > TraceLogger.MAX_SPAN_AGE_MS) { spanIdsToRemove.push(spanId); } } // Remove old spans for (const spanId of spanIdsToRemove) { this.spans.delete(spanId); } // Clean up active spans for removed correlation IDs // (If all spans for a correlation are removed, clean up the active tracking) const remainingCorrelationIds = new Set(Array.from(this.spans.values()).map((span) => span.correlationId)); const activeCorrelationIds = Array.from(this.activeSpans.keys()); for (const correlationId of activeCorrelationIds) { if (!remainingCorrelationIds.has(correlationId)) { this.activeSpans.delete(correlationId); } } // Limit total spans if still too many if (this.spans.size > TraceLogger.MAX_SPANS_PER_CORRELATION * 10) { // Keep only the most recent spans const spanArray = Array.from(this.spans.entries()); spanArray.sort((a, b) => (b[1].endTime?.getTime() || now) - (a[1].endTime?.getTime() || now)); // Keep only the newest MAX_SPANS_PER_CORRELATION * 10 spans const toKeep = spanArray.slice(0, TraceLogger.MAX_SPANS_PER_CORRELATION * 10); this.spans.clear(); for (const [spanId, span] of toKeep) { this.spans.set(spanId, span); } } } /** * Force run cleanup of old spans (public wrapper for tests) * * NOTE: Deterministic API used by tests to exercise cleanup branches. */ forceCleanupOldSpans() { this.cleanupOldSpans(); } /** * Generate a unique span ID */ generateSpanId() { return `span_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 10)}`; } /** * Find critical path through spans */ findCriticalPath(spans) { // Build dependency graph const graph = new Map(); const durations = new Map(); for (const span of spans) { durations.set(span.spanId, span.durationMs || 0); if (span.parentSpanId) { if (!graph.has(span.parentSpanId)) { graph.set(span.parentSpanId, []); } const children = graph.get(span.parentSpanId); if (children) { children.push(span.spanId); } } } // Find longest path using DFS let longestPath = []; let longestDuration = 0; function dfs(spanId, path, totalDuration) { path.push(spanId); totalDuration += durations.get(spanId) || 0; const children = graph.get(spanId) || []; if (children.length === 0) { if (totalDuration > longestDuration) { longestDuration = totalDuration; longestPath = [...path]; } } else { for (const child of children) { dfs(child, path, totalDuration); } } path.pop(); } // Start from root spans (those without parents) const rootSpans = spans.filter((s) => !s.parentSpanId); for (const root of rootSpans) { dfs(root.spanId, [], 0); } // Convert span IDs to tool names return longestPath.map((spanId) => spans.find((s) => s.spanId === spanId)?.toolName || spanId); } } /** * Singleton trace logger instance */ export const traceLogger = new TraceLogger(); /** * Helper: Create trace from A2A context execution log * * @param context - A2A context * @returns Trace data */ export function createTraceFromContext(context) { // Build a mapping from toolName to spanId for proper parent lookup const toolNameToSpanId = {}; const spans = context.executionLog.map((entry, index) => { const spanId = `span_${index}`; // Map this toolName to its spanId for future parent lookups if (entry.toolName) { toolNameToSpanId[entry.toolName] = spanId; } return { spanId, // Look up parent span ID from the toolName mapping parentSpanId: entry.parentToolName ? toolNameToSpanId[entry.parentToolName] : undefined, correlationId: context.correlationId, toolName: entry.toolName, startTime: new Date(entry.timestamp.getTime() - entry.durationMs), endTime: entry.timestamp, durationMs: entry.durationMs, depth: entry.depth, status: entry.status === "success" ? "success" : "error", error: entry.errorDetails, inputHash: entry.inputHash, outputSummary: entry.outputSummary, }; }); const totalDurationMs = spans.reduce((sum, span) => sum + (span.durationMs || 0), 0); return { correlationId: context.correlationId, spans, totalDurationMs, }; } //# sourceMappingURL=trace-logger.js.map