@454creative/easy-email
Version:
A framework-agnostic email service library for Node.js with observability and monitoring
203 lines (176 loc) • 5.48 kB
text/typescript
/**
* 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;
}
};
};
}