/ src / utils / log-rotator.js
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;