UNPKG

@454creative/easy-email

Version:

A framework-agnostic email service library for Node.js with observability and monitoring

203 lines (176 loc) 5.48 kB
/** * Performance monitoring utilities for the email library */ export interface PerformanceMetrics { operation: string; duration: number; timestamp: Date; success: boolean; error: string | undefined; metadata: Record<string, any> | undefined; } export interface PerformanceConfig { enabled: boolean; thresholdMs: number; logSlowOperations: boolean; trackMemoryUsage: boolean; } export class PerformanceMonitor { private static instance: PerformanceMonitor; private metrics: PerformanceMetrics[] = []; private config: PerformanceConfig; private startTimes: Map<string, number> = new Map(); private metadataStore: Map<string, string> = new Map(); private constructor(config: Partial<PerformanceConfig> = {}) { this.config = { enabled: true, thresholdMs: 1000, logSlowOperations: true, trackMemoryUsage: false, ...config }; } static getInstance(config?: Partial<PerformanceConfig>): PerformanceMonitor { if (!PerformanceMonitor.instance) { PerformanceMonitor.instance = new PerformanceMonitor(config); } return PerformanceMonitor.instance; } /** * Start timing an operation */ startOperation(operation: string, metadata?: Record<string, any>): void { if (!this.config.enabled) return; this.startTimes.set(operation, Date.now()); if (metadata) { this.metadataStore.set(operation, JSON.stringify(metadata)); } } /** * End timing an operation and record metrics */ endOperation(operation: string, success: boolean, error?: string): PerformanceMetrics | null { if (!this.config.enabled) return null; const startTime = this.startTimes.get(operation); if (!startTime) { console.warn(`PerformanceMonitor: No start time found for operation: ${operation}`); return null; } const duration = Date.now() - startTime; const timestamp = new Date(); const metric: PerformanceMetrics = { operation, duration, timestamp, success, error, metadata: this.getMetadata(operation) }; this.metrics.push(metric); this.startTimes.delete(operation); this.metadataStore.delete(operation); // Log slow operations if (this.config.logSlowOperations && duration > this.config.thresholdMs) { console.warn(`Slow operation detected: ${operation} took ${duration}ms`); } return metric; } /** * Get performance summary */ getSummary(): { totalOperations: number; averageDuration: number; slowOperations: PerformanceMetrics[]; successRate: number; topOperations: Array<{ operation: string; count: number; avgDuration: number }>; } { if (this.metrics.length === 0) { return { totalOperations: 0, averageDuration: 0, slowOperations: [], successRate: 0, topOperations: [] }; } const totalOperations = this.metrics.length; const averageDuration = this.metrics.reduce((sum, m) => sum + m.duration, 0) / totalOperations; const slowOperations = this.metrics.filter(m => m.duration > this.config.thresholdMs); const successRate = this.metrics.filter(m => m.success).length / totalOperations; // Group by operation const operationStats = new Map<string, { count: number; totalDuration: number }>(); this.metrics.forEach(metric => { const existing = operationStats.get(metric.operation) || { count: 0, totalDuration: 0 }; existing.count++; existing.totalDuration += metric.duration; operationStats.set(metric.operation, existing); }); const topOperations = Array.from(operationStats.entries()) .map(([operation, stats]) => ({ operation, count: stats.count, avgDuration: stats.totalDuration / stats.count })) .sort((a, b) => b.count - a.count) .slice(0, 10); return { totalOperations, averageDuration, slowOperations, successRate, topOperations }; } /** * Clear all metrics */ clearMetrics(): void { this.metrics = []; this.startTimes.clear(); } /** * Get all metrics */ getMetrics(): PerformanceMetrics[] { return [...this.metrics]; } /** * Update configuration */ updateConfig(config: Partial<PerformanceConfig>): void { this.config = { ...this.config, ...config }; } private getMetadata(operation: string): Record<string, any> | undefined { const metadataStr = this.metadataStore.get(operation); if (metadataStr) { try { return JSON.parse(metadataStr); } catch { return undefined; } } return undefined; } } /** * Performance decorator for methods */ export function trackPerformance(operationName?: string) { return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { const method = descriptor.value; const monitor = PerformanceMonitor.getInstance(); descriptor.value = async function (...args: any[]) { const operation = operationName || `${target.constructor.name}.${propertyName}`; monitor.startOperation(operation); try { const result = await method.apply(this, args); monitor.endOperation(operation, true); return result; } catch (error) { monitor.endOperation(operation, false, error instanceof Error ? error.message : String(error)); throw error; } }; }; }