UNPKG

logs-interceptor-node14

Version:

High-performance, production-ready log interceptor for Node.js 14 applications with Loki integration

546 lines 19.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseStackTrace = exports.extractErrorMetadata = exports.createCorrelationId = exports.validateConfig = exports.mergeConfigs = exports.loadConfigFromEnv = exports.calculateCompressionRatio = exports.formatBytes = exports.shouldSampleAdvanced = exports.shouldSample = exports.parseLabels = exports.hashSensitiveData = exports.sanitizeData = exports.detectSensitiveData = exports.safeStringify = void 0; const crypto = __importStar(require("crypto")); /** * Safely stringify any value, handling circular references and non-serializable objects */ function safeStringify(value, maxDepth = 10) { const seen = new WeakSet(); let depth = 0; try { return JSON.stringify(value, function (key, val) { // Check depth if (depth > maxDepth) { return '[Max Depth Reached]'; } if (val === null || val === undefined) { return val; } if (typeof val === 'object') { depth++; if (seen.has(val)) { return '[Circular Reference]'; } seen.add(val); // Handle special objects if (val instanceof Buffer) { return `[Buffer: ${val.length} bytes]`; } if (val instanceof Promise) { return '[Promise]'; } if (val instanceof WeakMap || val instanceof WeakSet) { return `[${val.constructor.name}]`; } } if (typeof val === 'function') { return `[Function: ${val.name || 'anonymous'}]`; } if (typeof val === 'symbol') { return `[Symbol: ${val.toString()}]`; } if (typeof val === 'bigint') { return val.toString() + 'n'; } if (val instanceof Error) { return { name: val.name, message: val.message, stack: val.stack, code: val.code, }; } if (val instanceof Date) { return val.toISOString(); } if (val instanceof RegExp) { return val.toString(); } if (val instanceof Map) { return { type: 'Map', entries: Array.from(val.entries()), }; } if (val instanceof Set) { return { type: 'Set', values: Array.from(val.values()), }; } return val; }, 2); } catch (error) { return `[Unserializable: ${error instanceof Error ? error.message : 'Unknown error'}]`; } } exports.safeStringify = safeStringify; /** * Detect sensitive data in a string */ function detectSensitiveData(text, patterns) { // Check against patterns for (const pattern of patterns) { if (pattern.test(text)) { return true; } } // Check for common sensitive patterns const commonPatterns = [ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, /\b(?:\d{4}[-\s]?){3}\d{4}\b/, /\b\d{3}-\d{2}-\d{4}\b/, /\b\d{3}\.\d{3}\.\d{3}-\d{2}\b/, /Bearer\s+[A-Za-z0-9\-._~+\/]+=*/i, /Basic\s+[A-Za-z0-9+\/]+=*/i, // Basic auth ]; for (const pattern of commonPatterns) { if (pattern.test(text)) { return true; } } return false; } exports.detectSensitiveData = detectSensitiveData; /** * Sanitize sensitive data from an object */ function sanitizeData(data, sensitivePatterns) { const sanitized = {}; for (const [key, value] of Object.entries(data)) { // Check if key matches sensitive patterns const isKeySensitive = sensitivePatterns.some(pattern => pattern.test(key)); if (isKeySensitive) { sanitized[key] = '[REDACTED]'; continue; } // Handle different value types if (typeof value === 'string') { if (detectSensitiveData(value, sensitivePatterns)) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } else if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { sanitized[key] = value.map(item => { if (typeof item === 'string' && detectSensitiveData(item, sensitivePatterns)) { return '[REDACTED]'; } if (typeof item === 'object' && item !== null) { return sanitizeData(item, sensitivePatterns); } return item; }); } else { sanitized[key] = sanitizeData(value, sensitivePatterns); } } else { sanitized[key] = value; } } return sanitized; } exports.sanitizeData = sanitizeData; /** * Hash sensitive data for tracking without exposing it */ function hashSensitiveData(data) { return crypto .createHash('sha256') .update(data) .digest('hex') .substring(0, 16); } exports.hashSensitiveData = hashSensitiveData; /** * Parse labels from environment variable string format * Format: "key1=value1,key2=value2" */ function parseLabels(labelsString) { const labels = {}; if (!labelsString) { return labels; } try { // Support JSON format if (labelsString.startsWith('{')) { return JSON.parse(labelsString); } // Support key=value format const pairs = labelsString.split(','); for (const pair of pairs) { const [key, ...valueParts] = pair.split('='); if (key && valueParts.length > 0) { labels[key.trim()] = valueParts.join('=').trim(); } } } catch (error) { console.warn('Failed to parse labels from environment:', error); } return labels; } exports.parseLabels = parseLabels; /** * Determine if a log should be sampled based on sampling rate */ function shouldSample(rate) { if (rate >= 1.0) return true; if (rate <= 0.0) return false; return Math.random() < rate; } exports.shouldSample = shouldSample; /** * Enhanced sampling with support for different strategies */ function shouldSampleAdvanced(rate, strategy = 'random', key) { if (rate >= 1.0) return true; if (rate <= 0.0) return false; switch (strategy) { case 'random': return Math.random() < rate; case 'deterministic': // Use a hash of the key to determine sampling if (!key) return Math.random() < rate; const hash = crypto.createHash('md5').update(key).digest(); const hashValue = hash.readUInt32BE(0) / 0xFFFFFFFF; return hashValue < rate; case 'adaptive': // Implement adaptive sampling based on load // This is a simplified version - in production, you'd track actual load const cpuUsage = process.cpuUsage(); const loadFactor = Math.min(1, cpuUsage.user / 1000000000); // Normalize to 0-1 const adjustedRate = rate * (1 - loadFactor * 0.5); // Reduce sampling under load return Math.random() < adjustedRate; default: return Math.random() < rate; } } exports.shouldSampleAdvanced = shouldSampleAdvanced; /** * Format bytes to human readable string */ function formatBytes(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } exports.formatBytes = formatBytes; /** * Calculate compression ratio */ function calculateCompressionRatio(original, compressed) { if (original === 0) return 0; return Math.round((1 - compressed / original) * 100); } exports.calculateCompressionRatio = calculateCompressionRatio; /** * Load configuration from environment variables */ function loadConfigFromEnv() { const env = process.env; const config = {}; // Transport configuration if (env.LOGS_INTERCEPTOR_URL) { if (!config.transport) config.transport = {}; config.transport.url = env.LOGS_INTERCEPTOR_URL; } if (env.LOGS_INTERCEPTOR_TENANT_ID) { if (!config.transport) config.transport = {}; config.transport.tenantId = env.LOGS_INTERCEPTOR_TENANT_ID; } if (env.LOGS_INTERCEPTOR_AUTH_TOKEN) { if (!config.transport) config.transport = {}; config.transport.authToken = env.LOGS_INTERCEPTOR_AUTH_TOKEN; } // Application metadata if (env.LOGS_INTERCEPTOR_APP_NAME) { config.appName = env.LOGS_INTERCEPTOR_APP_NAME; } if (env.LOGS_INTERCEPTOR_ENVIRONMENT) { config.environment = env.LOGS_INTERCEPTOR_ENVIRONMENT; } if (env.LOGS_INTERCEPTOR_VERSION) { config.version = env.LOGS_INTERCEPTOR_VERSION; } // Labels if (env.LOGS_INTERCEPTOR_LABELS) { config.labels = parseLabels(env.LOGS_INTERCEPTOR_LABELS); } // Buffer configuration if (env.LOGS_INTERCEPTOR_BUFFER_SIZE) { const bufferSize = parseInt(env.LOGS_INTERCEPTOR_BUFFER_SIZE, 10); if (!isNaN(bufferSize) && bufferSize > 0) { config.buffer = { ...config.buffer, maxSize: bufferSize, }; } } if (env.LOGS_INTERCEPTOR_MAX_MEMORY_MB) { const maxMemory = parseInt(env.LOGS_INTERCEPTOR_MAX_MEMORY_MB, 10); if (!isNaN(maxMemory) && maxMemory > 0) { config.buffer = { ...config.buffer, maxMemoryMB: maxMemory, }; } } if (env.LOGS_INTERCEPTOR_FLUSH_INTERVAL) { const flushInterval = parseInt(env.LOGS_INTERCEPTOR_FLUSH_INTERVAL, 10); if (!isNaN(flushInterval) && flushInterval > 0) { config.buffer = { ...config.buffer, flushInterval, }; } } // Filtering if (env.LOGS_INTERCEPTOR_LOG_LEVEL) { const levels = env.LOGS_INTERCEPTOR_LOG_LEVEL.split(',') .map(level => level.trim().toLowerCase()) .filter(level => ['debug', 'info', 'warn', 'error', 'fatal'].includes(level)); if (levels.length > 0) { config.filter = { ...config.filter, levels: levels, }; } } if (env.LOGS_INTERCEPTOR_SAMPLING_RATE) { const samplingRate = parseFloat(env.LOGS_INTERCEPTOR_SAMPLING_RATE); if (!isNaN(samplingRate) && samplingRate >= 0 && samplingRate <= 1) { config.filter = { ...config.filter, samplingRate, }; } } if (env.LOGS_INTERCEPTOR_SANITIZE) { config.filter = { ...config.filter, sanitize: env.LOGS_INTERCEPTOR_SANITIZE.toLowerCase() === 'true', }; } // Circuit breaker if (env.LOGS_INTERCEPTOR_CIRCUIT_BREAKER) { config.circuitBreaker = { enabled: env.LOGS_INTERCEPTOR_CIRCUIT_BREAKER.toLowerCase() === 'true', }; } // Feature flags if (env.LOGS_INTERCEPTOR_DEBUG) { config.debug = env.LOGS_INTERCEPTOR_DEBUG.toLowerCase() === 'true'; } if (env.LOGS_INTERCEPTOR_ENABLED) { const enabled = env.LOGS_INTERCEPTOR_ENABLED.toLowerCase() === 'true'; if (!enabled) { // Return a minimal config that effectively disables logging return { filter: { levels: [], }, }; } } return config; } exports.loadConfigFromEnv = loadConfigFromEnv; /** * Merge configurations with precedence: user config > env config > defaults */ function mergeConfigs(userConfig, envConfig) { const merged = { ...envConfig, ...userConfig, }; // Handle transport merge carefully if (envConfig.transport || userConfig.transport) { merged.transport = { ...envConfig.transport, ...userConfig.transport, }; } // Handle other nested objects if (envConfig.buffer || userConfig.buffer) { merged.buffer = { ...envConfig.buffer, ...userConfig.buffer }; } if (envConfig.filter || userConfig.filter) { merged.filter = { ...envConfig.filter, ...userConfig.filter }; } if (envConfig.labels || userConfig.labels) { merged.labels = { ...envConfig.labels, ...userConfig.labels }; } if (envConfig.dynamicLabels || userConfig.dynamicLabels) { merged.dynamicLabels = { ...envConfig.dynamicLabels, ...userConfig.dynamicLabels }; } if (envConfig.circuitBreaker || userConfig.circuitBreaker) { merged.circuitBreaker = { ...envConfig.circuitBreaker, ...userConfig.circuitBreaker }; } if (envConfig.integrations || userConfig.integrations) { merged.integrations = { ...envConfig.integrations, ...userConfig.integrations }; } if (envConfig.performance || userConfig.performance) { merged.performance = { ...envConfig.performance, ...userConfig.performance }; } return merged; } exports.mergeConfigs = mergeConfigs; /** * Validate that required configuration is present */ function validateConfig(config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const errors = []; if (!((_a = config.transport) === null || _a === void 0 ? void 0 : _a.url)) { errors.push('Transport URL is required (LOGS_INTERCEPTOR_URL)'); } if (!((_b = config.transport) === null || _b === void 0 ? void 0 : _b.tenantId)) { errors.push('Tenant ID is required (LOGS_INTERCEPTOR_TENANT_ID)'); } if (!config.appName) { errors.push('App name is required (LOGS_INTERCEPTOR_APP_NAME)'); } // Validate URL format if ((_c = config.transport) === null || _c === void 0 ? void 0 : _c.url) { try { new URL(config.transport.url); } catch { errors.push('Transport URL must be a valid URL'); } } // Validate buffer size if (((_d = config.buffer) === null || _d === void 0 ? void 0 : _d.maxSize) !== undefined && config.buffer.maxSize <= 0) { errors.push('Buffer max size must be greater than 0'); } // Validate flush interval if (((_e = config.buffer) === null || _e === void 0 ? void 0 : _e.flushInterval) !== undefined && config.buffer.flushInterval <= 0) { errors.push('Flush interval must be greater than 0'); } // Validate sampling rate if (((_f = config.filter) === null || _f === void 0 ? void 0 : _f.samplingRate) !== undefined) { const rate = config.filter.samplingRate; if (rate < 0 || rate > 1) { errors.push('Sampling rate must be between 0 and 1'); } } // Validate circuit breaker if (((_g = config.circuitBreaker) === null || _g === void 0 ? void 0 : _g.failureThreshold) !== undefined) { if (config.circuitBreaker.failureThreshold < 1) { errors.push('Circuit breaker failure threshold must be at least 1'); } } // Validate memory limit if (((_h = config.buffer) === null || _h === void 0 ? void 0 : _h.maxMemoryMB) !== undefined) { if (config.buffer.maxMemoryMB < 1 || config.buffer.maxMemoryMB > 1000) { errors.push('Max memory must be between 1 and 1000 MB'); } } // Validate compression level if (((_j = config.performance) === null || _j === void 0 ? void 0 : _j.compressionLevel) !== undefined) { const level = config.performance.compressionLevel; if (level < 0 || level > 9) { errors.push('Compression level must be between 0 and 9'); } } return errors; } exports.validateConfig = validateConfig; /** * Create a correlation ID for request tracking */ function createCorrelationId() { return crypto.randomBytes(16).toString('hex'); } exports.createCorrelationId = createCorrelationId; /** * Extract metadata from Error objects */ function extractErrorMetadata(error) { const metadata = { name: error.name, message: error.message, stack: error.stack, }; // Extract additional properties const errorObj = error; if (errorObj.code) metadata.code = errorObj.code; if (errorObj.statusCode) metadata.statusCode = errorObj.statusCode; if (errorObj.syscall) metadata.syscall = errorObj.syscall; if (errorObj.errno) metadata.errno = errorObj.errno; if (errorObj.path) metadata.path = errorObj.path; if (errorObj.address) metadata.address = errorObj.address; if (errorObj.port) metadata.port = errorObj.port; return metadata; } exports.extractErrorMetadata = extractErrorMetadata; /** * Parse stack trace to extract useful information */ function parseStackTrace(stack) { const lines = stack.split('\n'); const frames = []; for (const line of lines) { const match = line.match(/at\s+(.*?)\s+\((.*?):(\d+):(\d+)\)/); if (match) { frames.push({ function: match[1], file: match[2], line: parseInt(match[3], 10), column: parseInt(match[4], 10), }); } } return frames.slice(0, 10); // Limit to 10 frames } exports.parseStackTrace = parseStackTrace; //# sourceMappingURL=utils.js.map