From 36fe235bea66e64b93d97e69e586340ae293d885 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:10:55 +0200 Subject: [PATCH] feat(logging): implement scheduled logs command and enhance backup/task scheduling with cron checks --- app/Console/Commands/ViewScheduledLogs.php | 203 +++++++++++++++++++++ app/Console/Kernel.php | 50 +++++ config/logging.php | 14 ++ 3 files changed, 267 insertions(+) create mode 100644 app/Console/Commands/ViewScheduledLogs.php diff --git a/app/Console/Commands/ViewScheduledLogs.php b/app/Console/Commands/ViewScheduledLogs.php new file mode 100644 index 000000000..157afc7c1 --- /dev/null +++ b/app/Console/Commands/ViewScheduledLogs.php @@ -0,0 +1,203 @@ +option('date') ?: now()->format('Y-m-d'); + $logPaths = $this->getLogPaths($date); + + if (empty($logPaths)) { + $this->showAvailableLogFiles($date); + + return; + } + + $lines = $this->option('lines'); + $follow = $this->option('follow'); + + // Build grep filters + $filters = $this->buildFilters(); + $filterDescription = $this->getFilterDescription(); + $logTypeDescription = $this->getLogTypeDescription(); + + if ($follow) { + $this->info("Following {$logTypeDescription} logs for {$date}{$filterDescription} (Press Ctrl+C to stop)..."); + $this->line(''); + + if (count($logPaths) === 1) { + $logPath = $logPaths[0]; + if ($filters) { + passthru("tail -f {$logPath} | grep -E '{$filters}'"); + } else { + passthru("tail -f {$logPath}"); + } + } else { + // Multiple files - use multitail or tail with process substitution + $logPathsStr = implode(' ', $logPaths); + if ($filters) { + passthru("tail -f {$logPathsStr} | grep -E '{$filters}'"); + } else { + passthru("tail -f {$logPathsStr}"); + } + } + } else { + $this->info("Showing last {$lines} lines of {$logTypeDescription} logs for {$date}{$filterDescription}:"); + $this->line(''); + + if (count($logPaths) === 1) { + $logPath = $logPaths[0]; + if ($filters) { + passthru("tail -n {$lines} {$logPath} | grep -E '{$filters}'"); + } else { + passthru("tail -n {$lines} {$logPath}"); + } + } else { + // Multiple files - concatenate and sort by timestamp + $logPathsStr = implode(' ', $logPaths); + if ($filters) { + passthru("tail -n {$lines} {$logPathsStr} | sort | grep -E '{$filters}'"); + } else { + passthru("tail -n {$lines} {$logPathsStr} | sort"); + } + } + } + } + + private function getLogPaths(string $date): array + { + $paths = []; + + if ($this->option('errors')) { + // Error logs only + $errorPath = storage_path("logs/scheduled-errors-{$date}.log"); + if (File::exists($errorPath)) { + $paths[] = $errorPath; + } + } elseif ($this->option('all')) { + // Both normal and error logs + $normalPath = storage_path("logs/scheduled-{$date}.log"); + $errorPath = storage_path("logs/scheduled-errors-{$date}.log"); + + if (File::exists($normalPath)) { + $paths[] = $normalPath; + } + if (File::exists($errorPath)) { + $paths[] = $errorPath; + } + } else { + // Normal logs only (default) + $normalPath = storage_path("logs/scheduled-{$date}.log"); + if (File::exists($normalPath)) { + $paths[] = $normalPath; + } + } + + return $paths; + } + + private function showAvailableLogFiles(string $date): void + { + $logType = $this->getLogTypeDescription(); + $this->warn("No {$logType} logs found for date {$date}"); + + // Show available log files + $normalFiles = File::glob(storage_path('logs/scheduled-*.log')); + $errorFiles = File::glob(storage_path('logs/scheduled-errors-*.log')); + + if (! empty($normalFiles) || ! empty($errorFiles)) { + $this->info('Available scheduled log files:'); + + if (! empty($normalFiles)) { + $this->line(' Normal logs:'); + foreach ($normalFiles as $file) { + $basename = basename($file); + $this->line(" - {$basename}"); + } + } + + if (! empty($errorFiles)) { + $this->line(' Error logs:'); + foreach ($errorFiles as $file) { + $basename = basename($file); + $this->line(" - {$basename}"); + } + } + } + } + + private function getLogTypeDescription(): string + { + if ($this->option('errors')) { + return 'error'; + } elseif ($this->option('all')) { + return 'all'; + } else { + return 'normal'; + } + } + + private function buildFilters(): ?string + { + $filters = []; + + if ($taskName = $this->option('task-name')) { + $filters[] = '"task_name":"[^"]*'.preg_quote($taskName, '/').'[^"]*"'; + } + + if ($taskId = $this->option('task-id')) { + $filters[] = '"task_id":'.preg_quote($taskId, '/'); + } + + if ($backupName = $this->option('backup-name')) { + $filters[] = '"backup_name":"[^"]*'.preg_quote($backupName, '/').'[^"]*"'; + } + + if ($backupId = $this->option('backup-id')) { + $filters[] = '"backup_id":'.preg_quote($backupId, '/'); + } + + return empty($filters) ? null : implode('|', $filters); + } + + private function getFilterDescription(): string + { + $descriptions = []; + + if ($taskName = $this->option('task-name')) { + $descriptions[] = "task name: {$taskName}"; + } + + if ($taskId = $this->option('task-id')) { + $descriptions[] = "task ID: {$taskId}"; + } + + if ($backupName = $this->option('backup-name')) { + $descriptions[] = "backup name: {$backupName}"; + } + + if ($backupId = $this->option('backup-id')) { + $descriptions[] = "backup ID: {$backupId}"; + } + + return empty($descriptions) ? '' : ' (filtered by '.implode(', ', $descriptions).')'; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 395c58dee..190e46961 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -20,6 +20,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Server; use App\Models\Team; +use Cron\CronExpression; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Support\Carbon; @@ -197,6 +198,7 @@ class Kernel extends ConsoleKernel private function checkScheduledBackups(): void { $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); + if ($scheduled_backups->isEmpty()) { return; } @@ -238,10 +240,33 @@ class Kernel extends ConsoleKernel $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; } $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + + // Check if the backup should run now + $cron = new CronExpression($scheduled_backup->frequency); + $now = Carbon::now($serverTimezone); + + if ($cron->isDue($now)) { + Log::channel('scheduled')->info('Backup job running now', [ + 'backup_id' => $scheduled_backup->id, + 'backup_name' => $scheduled_backup->name ?? 'unnamed', + 'server_name' => $server->name, + 'frequency' => $scheduled_backup->frequency, + 'timezone' => $serverTimezone, + 'current_time' => $now->format('Y-m-d H:i:s T'), + ]); + } + $this->scheduleInstance->job(new DatabaseBackupJob( backup: $scheduled_backup ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error scheduling backup', [ + 'backup_id' => $scheduled_backup->id, + 'backup_name' => $scheduled_backup->name ?? 'unnamed', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); Log::error('Error scheduling backup: '.$e->getMessage()); Log::error($e->getTraceAsString()); } @@ -251,6 +276,7 @@ class Kernel extends ConsoleKernel private function checkScheduledTasks(): void { $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); + if ($scheduled_tasks->isEmpty()) { return; } @@ -301,10 +327,34 @@ class Kernel extends ConsoleKernel if (validate_timezone($serverTimezone) === false) { $serverTimezone = config('app.timezone'); } + + // Check if the task should run now + $cron = new CronExpression($scheduled_task->frequency); + $now = Carbon::now($serverTimezone); + + if ($cron->isDue($now)) { + Log::channel('scheduled')->info('Task job running now', [ + 'task_id' => $scheduled_task->id, + 'task_name' => $scheduled_task->name ?? 'unnamed', + 'server_name' => $server->name, + 'frequency' => $scheduled_task->frequency, + 'timezone' => $serverTimezone, + 'type' => $scheduled_task->service ? 'service' : 'application', + 'current_time' => $now->format('Y-m-d H:i:s T'), + ]); + } + $this->scheduleInstance->job(new ScheduledTaskJob( task: $scheduled_task ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error scheduling task', [ + 'task_id' => $scheduled_task->id, + 'task_name' => $scheduled_task->name ?? 'unnamed', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); Log::error('Error scheduling task: '.$e->getMessage()); Log::error($e->getTraceAsString()); } diff --git a/config/logging.php b/config/logging.php index 4c3df4ce1..a804295fa 100644 --- a/config/logging.php +++ b/config/logging.php @@ -118,6 +118,20 @@ return [ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], + + 'scheduled' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/scheduled.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 1, // Keep logs for 1 day only (truncated daily) + ], + + 'scheduled-errors' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/scheduled-errors.log'), + 'level' => 'error', + 'days' => 7, // Keep error logs for 7 days + ], ], ];