/** * Utility functions for logging operations */ import type { PerformanceTimer, LogMetadata } from './types'; /** * Create a performance timer to measure operation duration */ export function createTimer(operation: string): PerformanceTimer { const startTime = Date.now(); return { operation, startTime, end() { const endTime = Date.now(); return { operation, duration: endTime - startTime, startTime, endTime }; } }; } /** * Format error for logging */ export function formatError(error: Error | any): LogMetadata['error'] { if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack }; } if (typeof error === 'object' && error !== null) { return { name: error.constructor?.name || 'UnknownError', message: error.message || error.toString(), ...error }; } return { name: 'UnknownError', message: String(error) }; } /** * Sanitize sensitive data from log metadata */ export function sanitizeMetadata(metadata: LogMetadata): LogMetadata { const sensitiveKeys = [ 'password', 'token', 'secret', 'key', 'apiKey', 'api_key', 'authorization', 'auth', 'credential', 'credentials' ]; function sanitizeValue(value: any): any { if (typeof value === 'string') { return '[REDACTED]'; } if (Array.isArray(value)) { return value.map(sanitizeValue); } if (typeof value === 'object' && value !== null) { const sanitized: any = {}; for (const [key, val] of Object.entries(value)) { const lowerKey = key.toLowerCase(); if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = sanitizeValue(val); } } return sanitized; } return value; } const sanitized: LogMetadata = {}; for (const [key, value] of Object.entries(metadata)) { const lowerKey = key.toLowerCase(); if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = sanitizeValue(value); } } return sanitized; } /** * Generate a correlation ID for request tracking */ export function generateCorrelationId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Extract relevant HTTP request information for logging */ export function extractHttpMetadata(req: any): LogMetadata['request'] { if (!req) return undefined; return { method: req.method, url: req.url || req.originalUrl, userAgent: req.get?.('User-Agent') || req.headers?.['user-agent'], ip: req.ip || req.connection?.remoteAddress || req.headers?.['x-forwarded-for'] }; } /** * Create standardized business event metadata */ export function createBusinessEvent( type: string, action: string, options?: { entity?: string; result?: 'success' | 'failure' | 'partial'; amount?: number; symbol?: string; [key: string]: any; } ): LogMetadata { return { business: { type, action, ...options }, type: 'business_event' }; } /** * Create standardized security event metadata */ export function createSecurityEvent( type: 'authentication' | 'authorization' | 'access' | 'vulnerability', options?: { user?: string; resource?: string; action?: string; result?: 'success' | 'failure'; ip?: string; severity?: 'low' | 'medium' | 'high' | 'critical'; [key: string]: any; } ): LogMetadata { return { security: { type, ...options }, type: 'security_event' }; } /** * Mask sensitive data in strings */ export function maskSensitiveData(str: string): string { // Credit card numbers str = str.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '****-****-****-****'); // Email addresses str = str.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '***@***.***'); // API keys (common patterns) str = str.replace(/\b[A-Za-z0-9]{32,}\b/g, '[API_KEY]'); // JWT tokens str = str.replace(/eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g, '[JWT_TOKEN]'); return str; } /** * Calculate log entry size for batching */ export function calculateLogSize(entry: any): number { return JSON.stringify(entry).length; } /** * Throttle logging to prevent spam */ export class LogThrottle { private counters = new Map(); private readonly maxLogs: number; private readonly windowMs: number; constructor(maxLogs = 100, windowMs = 60000) { this.maxLogs = maxLogs; this.windowMs = windowMs; } shouldLog(key: string): boolean { const now = Date.now(); const counter = this.counters.get(key); if (!counter || now - counter.lastReset > this.windowMs) { this.counters.set(key, { count: 1, lastReset: now }); return true; } if (counter.count >= this.maxLogs) { return false; } counter.count++; return true; } reset(key?: string): void { if (key) { this.counters.delete(key); } else { this.counters.clear(); } } }