Skip to main content
Glama
performance.ts11 kB
/** * Performance Tracking Middleware * * Provides performance monitoring and timing for all MCP tool operations. * Tracks execution time, API latency, and performance bottlenecks. */ import { performance } from 'perf_hooks'; /** * Performance metrics for a single operation */ export interface PerformanceMetrics { toolName: string; startTime: number; endTime: number; duration: number; success: boolean; error?: string; metadata?: Record<string, unknown>; } /** * Performance thresholds for alerting */ export interface PerformanceThresholds { warning: number; // milliseconds critical: number; // milliseconds } /** * Performance summary statistics */ export interface PerformanceSummary { totalOperations: number; successfulOperations: number; failedOperations: number; averageDuration: number; minDuration: number; maxDuration: number; p50Duration: number; p95Duration: number; p99Duration: number; } /** * Performance tracking service */ export class PerformanceTracker { private static metrics: PerformanceMetrics[] = []; private static thresholds: Map<string, PerformanceThresholds> = new Map(); private static enabled: boolean = process.env.PERFORMANCE_TRACKING !== 'false'; private static readonly maxMetrics: number = parseInt( process.env.PERF_MAX_METRICS || '1000', 10 ); private static metricsIndex: number = 0; private static isBufferFull: boolean = false; /** * Set performance thresholds for a tool */ static setThresholds( toolName: string, thresholds: PerformanceThresholds ): void { this.thresholds.set(toolName, thresholds); } /** * Get default thresholds */ static getDefaultThresholds(): PerformanceThresholds { return { warning: 1000, // 1 second critical: 5000, // 5 seconds }; } /** * Start tracking an operation */ static startOperation( toolName: string, metadata?: Record<string, unknown> ): number { if (!this.enabled) return 0; const startTime = performance.now(); // Log slow operations in development if (process.env.NODE_ENV === 'development') { const thresholds = this.thresholds.get(toolName) || this.getDefaultThresholds(); const metadataHint = metadata && Object.keys(metadata).length > 0 ? ` metadata=${JSON.stringify(metadata)}` : ''; // Set a timeout to warn about slow operations setTimeout(() => { const duration = performance.now() - startTime; if (duration > thresholds.critical) { console.warn( `⚠️ Critical: ${toolName} is taking too long (${duration.toFixed( 2 )}ms)${metadataHint}` ); } else if (duration > thresholds.warning) { console.warn( `⚠️ Warning: ${toolName} is slow (${duration.toFixed(2)}ms)${metadataHint}` ); } }, thresholds.warning); } return startTime; } /** * End tracking an operation */ static endOperation( toolName: string, startTime: number, success: boolean = true, error?: string, metadata?: Record<string, unknown> ): PerformanceMetrics { if (!this.enabled) { return { toolName, startTime: 0, endTime: 0, duration: 0, success, }; } const endTime = performance.now(); const duration = endTime - startTime; const metrics: PerformanceMetrics = { toolName, startTime, endTime, duration, success, error, metadata, }; // Use circular buffer for efficient memory management if (this.isBufferFull) { // Overwrite oldest entry this.metrics[this.metricsIndex] = metrics; } else { // Still filling the buffer this.metrics.push(metrics); if (this.metrics.length >= this.maxMetrics) { this.isBufferFull = true; } } // Update circular buffer index this.metricsIndex = (this.metricsIndex + 1) % this.maxMetrics; // Log performance in development if (process.env.NODE_ENV === 'development') { const thresholds = this.thresholds.get(toolName) || this.getDefaultThresholds(); const statusIcon = success ? '✅' : '❌'; const timeColor = duration > thresholds.critical ? '🔴' : duration > thresholds.warning ? '🟡' : '🟢'; console.error( `${statusIcon} ${toolName}: ${timeColor} ${duration.toFixed(2)}ms` + (error ? ` (Error: ${error})` : '') ); } return metrics; } /** * Get performance summary for a tool */ static getSummary(toolName?: string): PerformanceSummary { const relevantMetrics = toolName ? this.metrics.filter((m) => m.toolName === toolName) : this.metrics; if (relevantMetrics.length === 0) { return { totalOperations: 0, successfulOperations: 0, failedOperations: 0, averageDuration: 0, minDuration: 0, maxDuration: 0, p50Duration: 0, p95Duration: 0, p99Duration: 0, }; } const durations = relevantMetrics .map((m) => m.duration) .sort((a, b) => a - b); const successCount = relevantMetrics.filter((m) => m.success).length; const failureCount = relevantMetrics.length - successCount; return { totalOperations: relevantMetrics.length, successfulOperations: successCount, failedOperations: failureCount, averageDuration: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0, minDuration: durations.length > 0 ? durations[0] : 0, maxDuration: durations.length > 0 ? durations[durations.length - 1] : 0, p50Duration: durations.length > 0 ? this.getPercentile(durations, 50) : 0, p95Duration: durations.length > 0 ? this.getPercentile(durations, 95) : 0, p99Duration: durations.length > 0 ? this.getPercentile(durations, 99) : 0, }; } /** * Get percentile value from sorted array */ private static getPercentile( sortedArray: number[], percentile: number ): number { if (sortedArray.length === 0) return 0; const index = Math.ceil((percentile / 100) * sortedArray.length) - 1; return sortedArray[Math.max(0, index)]; } /** * Clear all metrics */ static clear(): void { this.metrics = []; this.metricsIndex = 0; this.isBufferFull = false; } /** * Get all metrics */ static getMetrics(): PerformanceMetrics[] { return [...this.metrics]; } /** * Export metrics as JSON */ static exportMetrics(): string { return JSON.stringify( { timestamp: new Date().toISOString(), enabled: this.enabled, metrics: this.metrics, summary: this.getSummary(), }, null, 2 ); } /** * Enable or disable tracking */ static setEnabled(enabled: boolean): void { this.enabled = enabled; } /** * Check if tracking is enabled */ static isEnabled(): boolean { return this.enabled; } /** * Measure async function performance */ static async measureAsync<T>( toolName: string, fn: () => Promise<T>, metadata?: Record<string, unknown> ): Promise<T> { const startTime = this.startOperation(toolName, metadata); try { const result = await fn(); this.endOperation(toolName, startTime, true, undefined, metadata); return result; } catch (error: unknown) { this.endOperation( toolName, startTime, false, error instanceof Error ? error.message : String(error), metadata ); throw error; } } /** * Measure sync function performance */ static measure<T>( toolName: string, fn: () => T, metadata?: Record<string, unknown> ): T { const startTime = this.startOperation(toolName, metadata); try { const result = fn(); this.endOperation(toolName, startTime, true, undefined, metadata); return result; } catch (error: unknown) { this.endOperation( toolName, startTime, false, error instanceof Error ? error.message : String(error), metadata ); throw error; } } /** * Get slow operations above threshold */ static getSlowOperations(threshold?: number): PerformanceMetrics[] { const limit = threshold || 1000; // Default 1 second return this.metrics.filter((m) => m.duration > limit); } /** * Get failed operations */ static getFailedOperations(): PerformanceMetrics[] { return this.metrics.filter((m) => !m.success); } /** * Generate performance report */ static generateReport(): string { const summary = this.getSummary(); const slowOps = this.getSlowOperations(); const failedOps = this.getFailedOperations(); return ` Performance Report ================== Total Operations: ${summary.totalOperations} Successful: ${summary.successfulOperations} (${( (summary.successfulOperations / summary.totalOperations) * 100 ).toFixed(1)}%) Failed: ${summary.failedOperations} (${( (summary.failedOperations / summary.totalOperations) * 100 ).toFixed(1)}%) Timing Statistics ----------------- Average: ${summary.averageDuration.toFixed(2)}ms Min: ${summary.minDuration.toFixed(2)}ms Max: ${summary.maxDuration.toFixed(2)}ms P50: ${summary.p50Duration.toFixed(2)}ms P95: ${summary.p95Duration.toFixed(2)}ms P99: ${summary.p99Duration.toFixed(2)}ms Slow Operations: ${slowOps.length} Failed Operations: ${failedOps.length} `.trim(); } } /** * Performance monitoring decorator */ export function trackPerformance(toolName?: string) { return function ( target: unknown, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; const name = toolName || `${(target as Record<string, unknown>).constructor.name}.${propertyKey}`; descriptor.value = async function (...args: unknown[]) { const startTime = PerformanceTracker.startOperation(name); try { const result = await originalMethod.apply(this, args); PerformanceTracker.endOperation(name, startTime, true); return result; } catch (error: unknown) { PerformanceTracker.endOperation( name, startTime, false, error instanceof Error ? error.message : String(error) ); throw error; } }; return descriptor; }; } /** * Export a singleton instance for convenience */ export const performanceTracker = PerformanceTracker; // Re-export compatibility layer for instance-based usage export { PerformanceMonitor, performanceMonitor, } from './performance-compat.js';

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server