UNPKG

baseline-lint

Version:

Check web features for Baseline compatibility

571 lines (483 loc) 12.4 kB
// src/utils/logger.js // Comprehensive logging system with different levels and structured output import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Log levels in order of severity */ export const LogLevel = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3, TRACE: 4 }; /** * Log level names */ export const LogLevelNames = { [LogLevel.ERROR]: 'ERROR', [LogLevel.WARN]: 'WARN', [LogLevel.INFO]: 'INFO', [LogLevel.DEBUG]: 'DEBUG', [LogLevel.TRACE]: 'TRACE' }; /** * Log output formats */ export const LogFormat = { TEXT: 'text', JSON: 'json', COMPACT: 'compact' }; /** * Default logger configuration */ const DEFAULT_CONFIG = { level: LogLevel.INFO, format: LogFormat.TEXT, output: 'console', // 'console', 'file', 'both' filePath: null, maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5, enableColors: true, enableTimestamps: true, enableCallerInfo: false, bufferSize: 1000, flushInterval: 5000 // 5 seconds }; /** * ANSI color codes for console output */ const COLORS = { ERROR: '\x1b[31m', // Red WARN: '\x1b[33m', // Yellow INFO: '\x1b[36m', // Cyan DEBUG: '\x1b[35m', // Magenta TRACE: '\x1b[90m', // Gray RESET: '\x1b[0m', BOLD: '\x1b[1m', DIM: '\x1b[2m' }; /** * Main Logger class */ export class Logger { constructor(config = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.buffer = []; this.flushTimer = null; this.fileHandle = null; this.isInitialized = false; this.initialize(); } /** * Initialize the logger */ async initialize() { if (this.isInitialized) return; // Setup file output if needed if (this.config.output === 'file' || this.config.output === 'both') { await this.setupFileOutput(); } // Setup flush timer for buffered output if (this.config.bufferSize > 0) { this.setupFlushTimer(); } this.isInitialized = true; } /** * Setup file output */ async setupFileOutput() { if (!this.config.filePath) { // Default log file path const logDir = path.join(process.cwd(), 'logs'); await fs.mkdir(logDir, { recursive: true }); this.config.filePath = path.join(logDir, 'baseline-lint.log'); } try { this.fileHandle = await fs.open(this.config.filePath, 'a'); } catch (error) { console.error('Failed to open log file:', error.message); this.config.output = 'console'; } } /** * Setup flush timer for buffered output */ setupFlushTimer() { if (this.flushTimer) { clearInterval(this.flushTimer); } this.flushTimer = setInterval(() => { this.flush(); }, this.config.flushInterval); } /** * Log a message */ log(level, message, meta = {}) { if (level > this.config.level) return; const logEntry = this.createLogEntry(level, message, meta); if (this.config.bufferSize > 0) { this.buffer.push(logEntry); if (this.buffer.length >= this.config.bufferSize) { this.flush(); } } else { this.outputLogEntry(logEntry); } } /** * Create a log entry */ createLogEntry(level, message, meta) { const timestamp = this.config.enableTimestamps ? new Date().toISOString() : null; const callerInfo = this.config.enableCallerInfo ? this.getCallerInfo() : null; return { timestamp, level: LogLevelNames[level], levelNum: level, message, meta, caller: callerInfo, pid: process.pid }; } /** * Get caller information */ getCallerInfo() { const stack = new Error().stack; const lines = stack.split('\n'); // Skip the first few lines (Error, getCallerInfo, createLogEntry, log) const callerLine = lines[4]; if (!callerLine) return null; // Extract file and line number const match = callerLine.match(/at.*\(([^:]+):(\d+):(\d+)\)/); if (match) { return { file: path.basename(match[1]), line: parseInt(match[2]), column: parseInt(match[3]) }; } return null; } /** * Output a log entry */ outputLogEntry(entry) { const formatted = this.formatLogEntry(entry); if (this.config.output === 'console' || this.config.output === 'both') { this.outputToConsole(formatted, entry); } if ((this.config.output === 'file' || this.config.output === 'both') && this.fileHandle) { this.outputToFile(formatted); } } /** * Format a log entry */ formatLogEntry(entry) { switch (this.config.format) { case LogFormat.JSON: return JSON.stringify(entry); case LogFormat.COMPACT: return this.formatCompact(entry); case LogFormat.TEXT: default: return this.formatText(entry); } } /** * Format text output */ formatText(entry) { let output = ''; if (entry.timestamp) { output += `[${entry.timestamp}] `; } output += `[${entry.level}] `; if (entry.caller) { output += `[${entry.caller.file}:${entry.caller.line}] `; } output += entry.message; if (Object.keys(entry.meta).length > 0) { output += ` ${JSON.stringify(entry.meta)}`; } return output; } /** * Format compact output */ formatCompact(entry) { const time = entry.timestamp ? entry.timestamp.split('T')[1].split('.')[0] : ''; const caller = entry.caller ? `${entry.caller.file}:${entry.caller.line}` : ''; return `${time} ${entry.level} ${caller} ${entry.message}`; } /** * Output to console with colors */ outputToConsole(formatted, entry) { if (this.config.enableColors) { const color = this.getColorForLevel(entry.levelNum); console.log(`${color}${formatted}${COLORS.RESET}`); } else { console.log(formatted); } } /** * Output to file */ async outputToFile(formatted) { if (!this.fileHandle) return; try { await this.fileHandle.write(formatted + '\n'); // Check file size and rotate if needed const stats = await this.fileHandle.stat(); if (stats.size > this.config.maxFileSize) { await this.rotateLogFile(); } } catch (error) { console.error('Failed to write to log file:', error.message); } } /** * Get color for log level */ getColorForLevel(level) { switch (level) { case LogLevel.ERROR: return COLORS.ERROR; case LogLevel.WARN: return COLORS.WARN; case LogLevel.INFO: return COLORS.INFO; case LogLevel.DEBUG: return COLORS.DEBUG; case LogLevel.TRACE: return COLORS.TRACE; default: return COLORS.RESET; } } /** * Rotate log file */ async rotateLogFile() { if (!this.fileHandle) return; try { await this.fileHandle.close(); // Rotate existing log files for (let i = this.config.maxFiles - 1; i > 0; i--) { const oldFile = `${this.config.filePath}.${i}`; const newFile = `${this.config.filePath}.${i + 1}`; try { await fs.rename(oldFile, newFile); } catch (error) { // File might not exist, continue } } // Move current log to .1 await fs.rename(this.config.filePath, `${this.config.filePath}.1`); // Create new log file this.fileHandle = await fs.open(this.config.filePath, 'w'); } catch (error) { console.error('Failed to rotate log file:', error.message); } } /** * Flush buffered logs */ flush() { if (this.buffer.length === 0) return; const entries = [...this.buffer]; this.buffer = []; entries.forEach(entry => { this.outputLogEntry(entry); }); } /** * Convenience methods for different log levels */ error(message, meta = {}) { this.log(LogLevel.ERROR, message, meta); } warn(message, meta = {}) { this.log(LogLevel.WARN, message, meta); } info(message, meta = {}) { this.log(LogLevel.INFO, message, meta); } debug(message, meta = {}) { this.log(LogLevel.DEBUG, message, meta); } trace(message, meta = {}) { this.log(LogLevel.TRACE, message, meta); } /** * Create a child logger with additional context */ child(context = {}) { const childLogger = new Logger(this.config); const originalLog = childLogger.log.bind(childLogger); childLogger.log = (level, message, meta = {}) => { const mergedMeta = { ...context, ...meta }; originalLog(level, message, mergedMeta); }; return childLogger; } /** * Measure execution time */ time(label) { const start = process.hrtime.bigint(); return { end: (message = '') => { const end = process.hrtime.bigint(); const duration = Number(end - start) / 1000000; // Convert to milliseconds this.debug(`${label} completed in ${duration.toFixed(2)}ms`, { duration, message }); return duration; } }; } /** * Cleanup resources */ async close() { // Flush remaining logs this.flush(); // Clear flush timer if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } // Close file handle if (this.fileHandle) { await this.fileHandle.close(); this.fileHandle = null; } this.isInitialized = false; } } /** * Default logger instance */ export const logger = new Logger(); /** * Create a logger with specific configuration */ export function createLogger(config = {}) { return new Logger(config); } /** * Create a logger for a specific module/component */ export function createModuleLogger(moduleName, config = {}) { return new Logger({ ...config, enableCallerInfo: true }).child({ module: moduleName }); } /** * Performance logger for timing operations */ export class PerformanceLogger { constructor(logger) { this.logger = logger; this.metrics = new Map(); } start(label) { const start = process.hrtime.bigint(); this.metrics.set(label, start); this.logger.debug(`Performance: Started ${label}`); } end(label, message = '') { const start = this.metrics.get(label); if (!start) { this.logger.warn(`Performance: No start time found for ${label}`); return null; } const end = process.hrtime.bigint(); const duration = Number(end - start) / 1000000; // Convert to milliseconds this.metrics.delete(label); this.logger.info(`Performance: ${label} completed in ${duration.toFixed(2)}ms`, { label, duration, message }); return duration; } measure(label, fn) { return async (...args) => { this.start(label); try { const result = await fn(...args); this.end(label, 'success'); return result; } catch (error) { this.end(label, `error: ${error.message}`); throw error; } }; } } /** * Default performance logger */ export const performanceLogger = new PerformanceLogger(logger); /** * Structured logging helpers */ export const logHelpers = { /** * Log file operation */ fileOperation: (operation, filePath, meta = {}) => { logger.info(`File ${operation}: ${filePath}`, { operation, filePath, ...meta }); }, /** * Log parsing operation */ parsing: (fileType, filePath, issues, meta = {}) => { logger.info(`Parsed ${fileType} file: ${filePath}`, { fileType, filePath, issuesCount: issues.length, errors: issues.filter(i => i.severity === 'error').length, warnings: issues.filter(i => i.severity === 'warning').length, ...meta }); }, /** * Log baseline check */ baselineCheck: (feature, status, meta = {}) => { logger.debug(`Baseline check: ${feature}`, { feature, status, ...meta }); }, /** * Log cache operation */ cacheOperation: (operation, key, hit = null, meta = {}) => { logger.debug(`Cache ${operation}: ${key}`, { operation, key, hit, ...meta }); } }; /** * Export default logger for convenience */ export default logger;