option('days'); $cutoffDate = Carbon::now()->subDays($daysToKeep); $this->info("Trimming log files older than {$daysToKeep} days ({$cutoffDate->format('Y-m-d H:i:s')})..."); $totalSizeBefore = 0; $totalSizeAfter = 0; $filesProcessed = 0; foreach ($this->logFiles as $logFileName) { $logPath = storage_path('logs/' . $logFileName); if (!File::exists($logPath)) { // Normal: a task that is disabled or has never run writes no // log. Not worth a warning on every monthly run. continue; } $sizeBefore = File::size($logPath); $totalSizeBefore += $sizeBefore; $trimmed = $this->trimLogFile($logPath, $cutoffDate); $sizeAfter = File::size($logPath); $totalSizeAfter += $sizeAfter; $filesProcessed++; if ($trimmed) { $savedBytes = $sizeBefore - $sizeAfter; $savedKB = round($savedBytes / 1024, 2); $this->info("✓ {$logFileName}: Trimmed from " . $this->formatFileSize($sizeBefore) . " to " . $this->formatFileSize($sizeAfter) . " (saved {$savedKB} KB)"); } else { $this->info("✓ {$logFileName}: No entries older than {$daysToKeep} days (size: " . $this->formatFileSize($sizeBefore) . ")"); } } if ($filesProcessed > 0) { $totalSaved = $totalSizeBefore - $totalSizeAfter; $this->info("\nTotal: Processed {$filesProcessed} files, saved " . $this->formatFileSize($totalSaved)); } return 0; } /** * Trim log file to keep only entries newer than cutoff date. * * @param string $logPath * @param Carbon $cutoffDate * @return bool Whether any entries were removed */ protected function trimLogFile($logPath, $cutoffDate) { $content = File::get($logPath); $lines = explode("\n", $content); $keptLines = []; $removedCount = 0; foreach ($lines as $line) { // Skip empty lines if (trim($line) === '') { continue; } // Extract timestamp from log line format: [YYYY-MM-DD HH:MM:SS] message if (preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/', $line, $matches)) { $logDate = Carbon::parse($matches[1]); if ($logDate->greaterThanOrEqualTo($cutoffDate)) { $keptLines[] = $line; } else { $removedCount++; } } else { // Keep lines without timestamps: they are usually a // continuation of the timestamped line above (a stack trace, // wrapped output) and dropping them would corrupt the entry. $keptLines[] = $line; } } // Date-based trimming only works on files whose lines carry a // timestamp. appendOutputTo() writes a command's raw stdout, which // mostly does not ("Deleting expired disappearing messages..."), so // those files would keep every line forever. Fall back to a tail: keep // the most recent lines and drop the rest. if ($removedCount === 0 && count($keptLines) > self::MAX_UNDATED_LINES) { $dated = false; foreach ($keptLines as $line) { if (preg_match('/^\[\d{4}-\d{2}-\d{2} /', $line)) { $dated = true; break; } } if (!$dated) { $keptLines = array_slice($keptLines, -self::MAX_UNDATED_LINES); File::put($logPath, implode("\n", $keptLines) . "\n"); return true; } } if ($removedCount > 0) { // Rewrite the log file with only kept lines File::put($logPath, implode("\n", $keptLines) . "\n"); return true; } return false; } /** * Format file size in human-readable format. * * @param int $bytes * @return string */ protected function formatFileSize($bytes) { if ($bytes >= 1048576) { return round($bytes / 1048576, 2) . ' MB'; } elseif ($bytes >= 1024) { return round($bytes / 1024, 2) . ' KB'; } else { return $bytes . ' bytes'; } } }