UNPKG

alepm

Version:

Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features

466 lines (385 loc) 11.4 kB
const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const chalk = require('chalk'); class Logger { constructor(options = {}) { this.levels = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 }; this.colors = { error: 'red', warn: 'yellow', info: 'cyan', http: 'green', verbose: 'blue', debug: 'magenta', silly: 'gray' }; this.config = { level: options.level || 'info', silent: options.silent || false, timestamp: options.timestamp !== false, colorize: options.colorize !== false, json: options.json || false, logFile: options.logFile || path.join(os.homedir(), '.alepm', 'logs', 'alepm.log'), maxSize: options.maxSize || '10MB', maxFiles: options.maxFiles || 5, ...options }; this.init(); } async init() { await fs.ensureDir(path.dirname(this.config.logFile)); // Rotate logs if needed await this.rotateLogsIfNeeded(); } log(level, message, meta = {}) { if (this.config.silent) { return; } const levelNum = this.levels[level]; const configLevelNum = this.levels[this.config.level]; if (levelNum > configLevelNum) { return; } const logEntry = this.formatLogEntry(level, message, meta); // Output to console this.outputToConsole(level, logEntry); // Write to file this.writeToFile(logEntry); } error(message, meta = {}) { this.log('error', message, meta); } warn(message, meta = {}) { this.log('warn', message, meta); } info(message, meta = {}) { this.log('info', message, meta); } http(message, meta = {}) { this.log('http', message, meta); } verbose(message, meta = {}) { this.log('verbose', message, meta); } debug(message, meta = {}) { this.log('debug', message, meta); } silly(message, meta = {}) { this.log('silly', message, meta); } formatLogEntry(level, message, meta) { const timestamp = this.config.timestamp ? new Date().toISOString() : null; const entry = { timestamp, level, message, ...meta }; if (this.config.json) { return JSON.stringify(entry); } else { let formatted = ''; if (timestamp) { formatted += `[${timestamp}] `; } formatted += `${level.toUpperCase()}: ${message}`; if (Object.keys(meta).length > 0) { formatted += ` ${JSON.stringify(meta)}`; } return formatted; } } outputToConsole(level, logEntry) { const colorize = this.config.colorize && process.stdout.isTTY; if (colorize) { const color = this.colors[level] || 'white'; console.log(chalk[color](logEntry)); } else { console.log(logEntry); } } async writeToFile(logEntry) { try { await fs.appendFile(this.config.logFile, logEntry + '\n'); } catch (error) { // Fail silently to avoid infinite loops } } async rotateLogsIfNeeded() { try { const stats = await fs.stat(this.config.logFile); const maxSizeBytes = this.parseSize(this.config.maxSize); if (stats.size > maxSizeBytes) { await this.rotateLogs(); } } catch (error) { // File doesn't exist yet, no need to rotate } } async rotateLogs() { const logDir = path.dirname(this.config.logFile); const logBasename = path.basename(this.config.logFile, path.extname(this.config.logFile)); const logExt = path.extname(this.config.logFile); // Rotate existing files for (let i = this.config.maxFiles - 1; i > 0; i--) { const oldFile = path.join(logDir, `${logBasename}.${i}${logExt}`); const newFile = path.join(logDir, `${logBasename}.${i + 1}${logExt}`); if (await fs.pathExists(oldFile)) { if (i === this.config.maxFiles - 1) { // Delete the oldest file await fs.remove(oldFile); } else { await fs.move(oldFile, newFile); } } } // Move current log to .1 const currentLog = this.config.logFile; const rotatedLog = path.join(logDir, `${logBasename}.1${logExt}`); if (await fs.pathExists(currentLog)) { await fs.move(currentLog, rotatedLog); } } parseSize(size) { const match = size.match(/^(\d+(?:\.\d+)?)([KMGT]?)B$/i); if (!match) return 0; const [, value, unit] = match; const multipliers = { '': 1, K: 1024, M: 1024**2, G: 1024**3, T: 1024**4 }; return parseFloat(value) * (multipliers[unit.toUpperCase()] || 1); } // Performance logging time(label) { if (!this.timers) { this.timers = new Map(); } this.timers.set(label, process.hrtime.bigint()); } timeEnd(label) { if (!this.timers || !this.timers.has(label)) { this.warn(`Timer "${label}" does not exist`); return; } const start = this.timers.get(label); const end = process.hrtime.bigint(); const duration = Number(end - start) / 1000000; // Convert to milliseconds this.timers.delete(label); this.info(`${label}: ${duration.toFixed(2)}ms`); return duration; } // Request logging logRequest(method, url, options = {}) { this.http(`${method} ${url}`, { method, url, userAgent: options.userAgent, timeout: options.timeout, headers: this.sanitizeHeaders(options.headers) }); } logResponse(method, url, statusCode, duration, options = {}) { const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warn' : 'http'; this.log(level, `${method} ${url} ${statusCode} ${duration}ms`, { method, url, statusCode, duration, size: options.size }); } sanitizeHeaders(headers = {}) { const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']; const sanitized = {}; for (const [key, value] of Object.entries(headers)) { if (sensitiveHeaders.includes(key.toLowerCase())) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } return sanitized; } // Package operation logging logPackageOperation(operation, packageName, version, options = {}) { this.info(`${operation} ${packageName}@${version}`, { operation, package: packageName, version, ...options }); } logPackageError(operation, packageName, version, error, options = {}) { this.error(`Failed to ${operation} ${packageName}@${version}: ${error.message}`, { operation, package: packageName, version, error: error.message, stack: error.stack, ...options }); } logCacheOperation(operation, key, options = {}) { this.debug(`Cache ${operation}: ${key}`, { operation, key, ...options }); } logSecurityEvent(event, details = {}) { this.warn(`Security event: ${event}`, { event, timestamp: Date.now(), ...details }); } // Structured logging for analytics logAnalytics(event, data = {}) { this.info(`Analytics: ${event}`, { event, timestamp: Date.now(), session: this.getSessionId(), platform: process.platform, arch: process.arch, node: process.version, ...data }); } getSessionId() { if (!this.sessionId) { this.sessionId = require('crypto').randomUUID(); } return this.sessionId; } // Progress logging createProgressLogger(total, label = 'Progress') { let current = 0; let lastLogTime = 0; const minLogInterval = 1000; // Log at most once per second return { tick: (amount = 1) => { current += amount; const now = Date.now(); if (now - lastLogTime > minLogInterval || current >= total) { const percentage = ((current / total) * 100).toFixed(1); this.info(`${label}: ${current}/${total} (${percentage}%)`); lastLogTime = now; } }, complete: () => { this.info(`${label}: Complete (${total}/${total})`); } }; } // Error aggregation reportErrors() { if (!this.errorStats) { return { totalErrors: 0, errorTypes: {} }; } return { totalErrors: this.errorStats.total, errorTypes: { ...this.errorStats.types }, lastError: this.errorStats.lastError }; } trackError(error) { if (!this.errorStats) { this.errorStats = { total: 0, types: {}, lastError: null }; } this.errorStats.total++; this.errorStats.types[error.constructor.name] = (this.errorStats.types[error.constructor.name] || 0) + 1; this.errorStats.lastError = { message: error.message, timestamp: Date.now() }; } // Log file management async clearLogs() { try { const logDir = path.dirname(this.config.logFile); const logBasename = path.basename(this.config.logFile, path.extname(this.config.logFile)); const logExt = path.extname(this.config.logFile); // Remove all log files await fs.remove(this.config.logFile); for (let i = 1; i <= this.config.maxFiles; i++) { const logFile = path.join(logDir, `${logBasename}.${i}${logExt}`); if (await fs.pathExists(logFile)) { await fs.remove(logFile); } } this.info('Log files cleared'); } catch (error) { this.error('Failed to clear logs', { error: error.message }); } } async getLogStats() { try { const stats = { files: [], totalSize: 0 }; const logDir = path.dirname(this.config.logFile); const logBasename = path.basename(this.config.logFile, path.extname(this.config.logFile)); const logExt = path.extname(this.config.logFile); // Check main log file if (await fs.pathExists(this.config.logFile)) { const stat = await fs.stat(this.config.logFile); stats.files.push({ file: this.config.logFile, size: stat.size, modified: stat.mtime }); stats.totalSize += stat.size; } // Check rotated log files for (let i = 1; i <= this.config.maxFiles; i++) { const logFile = path.join(logDir, `${logBasename}.${i}${logExt}`); if (await fs.pathExists(logFile)) { const stat = await fs.stat(logFile); stats.files.push({ file: logFile, size: stat.size, modified: stat.mtime }); stats.totalSize += stat.size; } } return stats; } catch (error) { this.error('Failed to get log stats', { error: error.message }); return { files: [], totalSize: 0 }; } } // Configuration updates updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } setLevel(level) { if (!Object.prototype.hasOwnProperty.call(this.levels, level)) { throw new Error(`Invalid log level: ${level}`); } this.config.level = level; } setSilent(silent = true) { this.config.silent = silent; } setColorize(colorize = true) { this.config.colorize = colorize; } setJson(json = true) { this.config.json = json; } } module.exports = Logger;