log-rotator.js
1 /** 2 * Log rotation utility 3 * Cleans up old log files, keeping only the last N days (default: 7) 4 */ 5 6 import fs from 'fs'; 7 import path from 'path'; 8 import Logger from './logger.js'; 9 10 const logger = new Logger('log-rotator', { logToFile: false }); 11 12 /** 13 * Get all log files in a directory 14 * @param {string} logDir - Directory containing log files 15 * @returns {Array<{file: string, date: Date, age: number}>} Array of log file info 16 */ 17 function getLogFiles(logDir) { 18 if (!fs.existsSync(logDir)) { 19 return []; 20 } 21 22 const files = fs.readdirSync(logDir); 23 const logFiles = []; 24 const now = Date.now(); 25 26 for (const file of files) { 27 // Only process .log files 28 if (!file.endsWith('.log')) { 29 continue; 30 } 31 32 const filePath = path.join(logDir, file); 33 const stats = fs.statSync(filePath); 34 35 // Calculate age in days 36 const ageMs = now - stats.mtime.getTime(); 37 const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24)); 38 39 logFiles.push({ 40 file: filePath, 41 name: file, 42 date: stats.mtime, 43 age: ageDays, 44 size: stats.size, 45 }); 46 } 47 48 return logFiles; 49 } 50 51 /** 52 * Rotate logs, removing files older than retentionDays 53 * @param {Object} options - Rotation options 54 * @param {string} options.logDir - Directory containing log files (default: ./logs) 55 * @param {number} options.retentionDays - Number of days to keep (default: 7) 56 * @param {boolean} options.dryRun - If true, only show what would be deleted (default: false) 57 * @returns {Object} Statistics about the rotation 58 */ 59 export function rotateLogs(options = {}) { 60 const logDir = options.logDir || './logs'; 61 const retentionDays = options.retentionDays || 7; 62 const dryRun = options.dryRun || false; 63 64 logger.info(`Starting log rotation (retention: ${retentionDays} days, dry-run: ${dryRun})`); 65 66 const logFiles = getLogFiles(logDir); 67 logger.info(`Found ${logFiles.length} log files in ${logDir}`); 68 69 const toDelete = logFiles.filter(f => f.age > retentionDays); 70 const toKeep = logFiles.filter(f => f.age <= retentionDays); 71 72 if (toDelete.length === 0) { 73 logger.info('No log files to delete'); 74 return { 75 deleted: 0, 76 kept: toKeep.length, 77 freedSpace: 0, 78 }; 79 } 80 81 let totalFreedSpace = 0; 82 83 logger.info(`Files to delete (${toDelete.length}):`); 84 for (const file of toDelete) { 85 const sizeMB = (file.size / (1024 * 1024)).toFixed(2); 86 logger.info(` - ${file.name} (age: ${file.age} days, size: ${sizeMB} MB)`); 87 88 if (!dryRun) { 89 try { 90 fs.unlinkSync(file.file); 91 totalFreedSpace += file.size; 92 logger.success(`Deleted: ${file.name}`); 93 } catch (err) { 94 logger.error(`Failed to delete ${file.name}`, err); 95 } 96 } 97 } 98 99 const freedSpaceMB = (totalFreedSpace / (1024 * 1024)).toFixed(2); 100 101 if (dryRun) { 102 logger.info(`[DRY RUN] Would delete ${toDelete.length} files, freeing ${freedSpaceMB} MB`); 103 } else { 104 logger.success(`Deleted ${toDelete.length} log files, freed ${freedSpaceMB} MB`); 105 } 106 107 logger.info(`Keeping ${toKeep.length} recent log files`); 108 109 return { 110 deleted: dryRun ? 0 : toDelete.length, 111 kept: toKeep.length, 112 freedSpace: totalFreedSpace, 113 }; 114 } 115 116 /** 117 * CLI entry point 118 */ 119 if (import.meta.url === `file://${process.argv[1]}`) { 120 const args = process.argv.slice(2); 121 const dryRun = args.includes('--dry-run'); 122 const retentionDays = parseInt(args.find(a => a.startsWith('--days='))?.split('=')[1]) || 7; 123 const logDir = args.find(a => a.startsWith('--dir='))?.split('=')[1] || './logs'; 124 125 try { 126 rotateLogs({ logDir, retentionDays, dryRun }); 127 process.exit(0); 128 } catch (err) { 129 logger.error('Log rotation failed', err); 130 process.exit(1); 131 } 132 } 133 134 export default rotateLogs;