feat(logging): implement scheduled logs command and enhance backup/task scheduling with cron checks
This commit is contained in:
203
app/Console/Commands/ViewScheduledLogs.php
Normal file
203
app/Console/Commands/ViewScheduledLogs.php
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
class ViewScheduledLogs extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'logs:scheduled
|
||||||
|
{--lines=50 : Number of lines to display}
|
||||||
|
{--follow : Follow the log file (tail -f)}
|
||||||
|
{--date= : Specific date (Y-m-d format, defaults to today)}
|
||||||
|
{--task-name= : Filter by task name (partial match)}
|
||||||
|
{--task-id= : Filter by task ID}
|
||||||
|
{--backup-name= : Filter by backup name (partial match)}
|
||||||
|
{--backup-id= : Filter by backup ID}
|
||||||
|
{--errors : View error logs only}
|
||||||
|
{--all : View both normal and error logs}';
|
||||||
|
|
||||||
|
protected $description = 'View scheduled backups and tasks logs with optional filtering';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$date = $this->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).')';
|
||||||
|
}
|
||||||
|
}
|
@@ -20,6 +20,7 @@ use App\Models\ScheduledDatabaseBackup;
|
|||||||
use App\Models\ScheduledTask;
|
use App\Models\ScheduledTask;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
|
use Cron\CronExpression;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -197,6 +198,7 @@ class Kernel extends ConsoleKernel
|
|||||||
private function checkScheduledBackups(): void
|
private function checkScheduledBackups(): void
|
||||||
{
|
{
|
||||||
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
|
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
|
||||||
|
|
||||||
if ($scheduled_backups->isEmpty()) {
|
if ($scheduled_backups->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -238,10 +240,33 @@ class Kernel extends ConsoleKernel
|
|||||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||||
}
|
}
|
||||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
$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(
|
$this->scheduleInstance->job(new DatabaseBackupJob(
|
||||||
backup: $scheduled_backup
|
backup: $scheduled_backup
|
||||||
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} 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('Error scheduling backup: '.$e->getMessage());
|
||||||
Log::error($e->getTraceAsString());
|
Log::error($e->getTraceAsString());
|
||||||
}
|
}
|
||||||
@@ -251,6 +276,7 @@ class Kernel extends ConsoleKernel
|
|||||||
private function checkScheduledTasks(): void
|
private function checkScheduledTasks(): void
|
||||||
{
|
{
|
||||||
$scheduled_tasks = ScheduledTask::where('enabled', true)->get();
|
$scheduled_tasks = ScheduledTask::where('enabled', true)->get();
|
||||||
|
|
||||||
if ($scheduled_tasks->isEmpty()) {
|
if ($scheduled_tasks->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -301,10 +327,34 @@ class Kernel extends ConsoleKernel
|
|||||||
if (validate_timezone($serverTimezone) === false) {
|
if (validate_timezone($serverTimezone) === false) {
|
||||||
$serverTimezone = config('app.timezone');
|
$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(
|
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||||
task: $scheduled_task
|
task: $scheduled_task
|
||||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} 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('Error scheduling task: '.$e->getMessage());
|
||||||
Log::error($e->getTraceAsString());
|
Log::error($e->getTraceAsString());
|
||||||
}
|
}
|
||||||
|
@@ -118,6 +118,20 @@ return [
|
|||||||
'emergency' => [
|
'emergency' => [
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'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
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
Reference in New Issue
Block a user