176 lines
5.9 KiB
PHP
176 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class TrimInactiveProfileLogs extends Command
|
|
{
|
|
protected $signature = 'profiles:trim-logs {--days=30 : Number of days of logs to keep}';
|
|
|
|
protected $description = 'Trim inactive profile log files to retain only recent entries';
|
|
|
|
/**
|
|
* Every log written by the scheduler through appendOutputTo().
|
|
*
|
|
* appendOutputTo() never rotates - it appends for the life of the server.
|
|
* Laravel's own daily driver rotates laravel-*.log, but these files sit
|
|
* outside it, so without this command they grow without bound. On
|
|
* production wirechat-cleanup.log had reached 4.4 MB from a task that runs
|
|
* every five minutes, and bounce-processing.log 1.8 MB.
|
|
*
|
|
* Keep in step with the appendOutputTo() calls in Console\Kernel.
|
|
*/
|
|
/**
|
|
* Lines kept in a log that carries no timestamps, so it can still be
|
|
* capped. Roughly a fortnight of a five-minute task at two lines a run.
|
|
*/
|
|
private const MAX_UNDATED_LINES = 10000;
|
|
|
|
protected $logFiles = [
|
|
'inactive-profiles.log',
|
|
'mark-inactive-profiles.log',
|
|
'mark-incomplete-profiles.log',
|
|
'permanent-deletions.log',
|
|
'wirechat-cleanup.log',
|
|
'bounce-processing.log',
|
|
'scout-reindex.log',
|
|
'call-expiry.log',
|
|
'cert-monitor.log',
|
|
'ip-cleanup.log',
|
|
];
|
|
|
|
public function handle()
|
|
{
|
|
$daysToKeep = (int) $this->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';
|
|
}
|
|
}
|
|
}
|