MCP GitHub Issue Server
by sammcj
- src
- server
/**
* Request tracing for monitoring and debugging with memory management
*/
import { Logger } from '../logging/index.js';
export interface TracerConfig {
maxTraces?: number; // Maximum number of traces to store
maxEventsPerTrace?: number; // Maximum events per trace
traceRetentionMs?: number; // How long to keep traces
cleanupIntervalMs?: number; // How often to run cleanup
}
const DEFAULT_CONFIG: TracerConfig = {
maxTraces: 1000,
maxEventsPerTrace: 100,
traceRetentionMs: 24 * 60 * 60 * 1000, // 24 hours
cleanupIntervalMs: 60 * 60 * 1000, // 1 hour
};
export interface TraceEvent {
type: string;
tool?: string;
timestamp: number;
success?: boolean;
error?: string;
duration?: number;
[key: string]: unknown;
}
export class RequestTracer {
private traces: Map<string, TraceEvent[]> = new Map();
private startTimes: Map<string, number> = new Map();
private logger: Logger;
private config: TracerConfig;
private cleanupTimer!: NodeJS.Timeout;
constructor(config: TracerConfig = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.logger = Logger.getInstance().child({
component: 'RequestTracer',
context: {
maxTraces: this.config.maxTraces,
maxEventsPerTrace: this.config.maxEventsPerTrace,
retentionMs: this.config.traceRetentionMs,
},
});
// Start cleanup timer
this.cleanupTimer = setInterval(
() => this.cleanup(),
this.config.cleanupIntervalMs ?? DEFAULT_CONFIG.cleanupIntervalMs!
) as unknown as NodeJS.Timeout;
// Bind cleanup to process events
process.on('SIGINT', () => this.destroy());
process.on('SIGTERM', () => this.destroy());
this.logger.info('Request tracer initialized', {
config: this.config,
context: {
operation: 'initialize',
timestamp: Date.now(),
},
});
}
/**
* Cleanup old traces and enforce size limits
*/
private cleanup(): void {
const cleanupStart = Date.now();
const cutoff =
cleanupStart - (this.config.traceRetentionMs ?? DEFAULT_CONFIG.traceRetentionMs!);
let removedCount = 0;
let removedEvents = 0;
// Remove old traces
for (const [requestId, events] of this.traces.entries()) {
const startTime = this.startTimes.get(requestId);
if (startTime && startTime < cutoff) {
removedEvents += events.length;
removedCount++;
this.traces.delete(requestId);
this.startTimes.delete(requestId);
}
}
// Enforce maximum traces limit
const maxTraces = this.config.maxTraces ?? DEFAULT_CONFIG.maxTraces!;
if (this.traces.size > maxTraces) {
const sortedTraces = Array.from(this.startTimes.entries()).sort(([, a], [, b]) => b - a);
const tracesToRemove = sortedTraces.slice(maxTraces);
for (const [requestId] of tracesToRemove) {
const events = this.traces.get(requestId) || [];
removedEvents += events.length;
removedCount++;
this.traces.delete(requestId);
this.startTimes.delete(requestId);
}
}
if (removedCount > 0) {
this.logger.info('Traces cleaned up', {
removedTraces: removedCount,
removedEvents,
remainingTraces: this.traces.size,
duration: Date.now() - cleanupStart,
context: {
operation: 'cleanup',
timestamp: cleanupStart,
cutoff,
},
});
}
}
startTrace(requestId: string, event: TraceEvent): void {
// Cleanup if we're at the limit
if (this.traces.size >= (this.config.maxTraces ?? DEFAULT_CONFIG.maxTraces!)) {
this.cleanup();
}
const enrichedEvent = {
...event,
context: {
operation: 'startTrace',
requestId,
timestamp: event.timestamp,
tool: event.tool,
traceSize: this.traces.size,
},
};
this.traces.set(requestId, [enrichedEvent]);
this.startTimes.set(requestId, event.timestamp);
this.logger.debug('Trace started', {
requestId,
event: enrichedEvent,
stats: {
activeTraces: this.traces.size,
memoryUsage: process.memoryUsage().heapUsed,
},
});
}
addEvent(requestId: string, event: TraceEvent): void {
const events = this.traces.get(requestId) || [];
const startTime = this.startTimes.get(requestId);
// Enforce maximum events per trace
const maxEvents = this.config.maxEventsPerTrace ?? DEFAULT_CONFIG.maxEventsPerTrace!;
if (events.length >= maxEvents) {
this.logger.warn('Maximum events per trace reached', {
requestId,
limit: maxEvents,
context: {
operation: 'addEvent',
timestamp: event.timestamp,
eventCount: events.length,
traceDuration: startTime ? event.timestamp - startTime : undefined,
},
});
return;
}
const enrichedEvent = {
...event,
context: {
operation: 'addEvent',
requestId,
timestamp: event.timestamp,
tool: event.tool,
eventIndex: events.length,
traceDuration: startTime ? event.timestamp - startTime : undefined,
},
};
events.push(enrichedEvent);
this.traces.set(requestId, events);
this.logger.debug('Event added to trace', {
requestId,
event: enrichedEvent,
stats: {
eventCount: events.length,
traceDuration: startTime ? event.timestamp - startTime : undefined,
},
});
}
endTrace(requestId: string, event: TraceEvent): void {
const events = this.traces.get(requestId) || [];
const startTime = this.startTimes.get(requestId);
const duration = startTime ? event.timestamp - startTime : undefined;
const enrichedEvent = {
...event,
duration,
context: {
operation: 'endTrace',
requestId,
timestamp: event.timestamp,
tool: event.tool,
eventCount: events.length + 1,
traceDuration: duration,
success: event.success,
error: event.error,
},
};
events.push(enrichedEvent);
this.traces.set(requestId, events);
const logLevel = event.error ? 'warn' : 'info';
this.logger[logLevel]('Trace completed', {
requestId,
event: enrichedEvent,
stats: {
eventCount: events.length,
duration,
success: event.success,
error: event.error,
},
});
}
getTrace(requestId: string): TraceEvent[] {
return this.traces.get(requestId) || [];
}
getStartTime(requestId: string): number | undefined {
return this.startTimes.get(requestId);
}
clearTrace(requestId: string): void {
const events = this.traces.get(requestId);
const startTime = this.startTimes.get(requestId);
this.traces.delete(requestId);
this.startTimes.delete(requestId);
this.logger.debug('Trace cleared', {
requestId,
stats: {
eventCount: events?.length || 0,
duration: startTime ? Date.now() - startTime : undefined,
},
});
}
/**
* Get memory usage statistics
*/
getStats(): {
traceCount: number;
totalEvents: number;
memoryUsage: NodeJS.MemoryUsage;
} {
let totalEvents = 0;
for (const events of this.traces.values()) {
totalEvents += events.length;
}
const stats = {
traceCount: this.traces.size,
totalEvents,
memoryUsage: process.memoryUsage(),
};
this.logger.debug('Stats retrieved', {
stats,
context: {
operation: 'getStats',
timestamp: Date.now(),
},
});
return stats;
}
/**
* Cleanup resources and stop timers
*/
destroy(): void {
const stats = this.getStats();
clearInterval(this.cleanupTimer);
this.traces.clear();
this.startTimes.clear();
this.logger.info('Request tracer destroyed', {
stats,
context: {
operation: 'destroy',
timestamp: Date.now(),
},
});
}
}