spatie/laravel-schedule-monitor
Monitor Laravel scheduled tasks by logging starts, finishes, failures, and skips to a database table and viewing run history via an Artisan command. Optionally sync with Oh Dear to get alerts when tasks fail or don’t run on time.
Installation:
composer require spatie/laravel-schedule-monitor
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-migrations"
php artisan migrate
Publish the config file (optional but recommended):
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="schedule-monitor-config"
First Sync: Sync your Laravel schedule with the monitor:
php artisan schedule-monitor:sync
First Check: Verify monitored tasks:
php artisan schedule-monitor:list
If a scheduled task fails to run, use:
php artisan schedule-monitor:list
Look for tasks with red backgrounds (late/failed) and inspect their log entries in the monitored_scheduled_task_log_items table.
Schedule Definition (in app/Console/Kernel.php):
protected function schedule(Schedule $schedule) {
$schedule->command('backup:run')->daily()
->monitorName('daily-backup')
->graceTimeInMinutes(10)
->storeOutputInDb();
$schedule->job(new ProcessInvoices())->hourly()
->doNotMonitor(); // Skip monitoring for this task
}
Sync After Deployment: Add to your deployment script:
php artisan schedule-monitor:sync
Monitoring:
schedule-monitor:list for real-time status.monitored_scheduled_task_log_items for historical data:
$logs = MonitoredScheduledTaskLogItem::where('task_name', 'daily-backup')
->orderBy('created_at', 'desc')
->take(10)
->get();
.env:
OH_DEAR_API_TOKEN=your_token
OH_DEAR_MONITOR_ID=12345
OH_DEAR_QUEUE=ohdear
php artisan schedule-monitor:verify
php artisan schedule-monitor:sync --keep-old
(Use --keep-old to avoid removing existing Oh Dear monitors.)Dynamic Grace Time:
Override grace time per task or globally in config/schedule-monitor.php:
'default_grace_time' => 15, // Global default
$schedule->command('long-task')->daily()->graceTimeInMinutes(30);
Conditional Monitoring: Use middleware to skip monitoring for specific tasks:
$schedule->command('test:slow')->daily()
->when(function () {
return config('app.env') === 'production';
})
->monitorName('prod-slow-test');
Log Pruning:
Automate log cleanup in app/Console/Kernel.php:
$schedule->command('model:prune', [
'--model' => \Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem::class,
])->daily();
Missing Sync:
schedule-monitor:list but not in Oh Dear.schedule-monitor:sync after every deployment or schedule change.--verbose flag for sync commands to see skipped tasks.Grace Time Mismatch:
graceTimeInMinutes per task or set a global default in config.storeOutputInDb to inspect task duration:
$log = MonitoredScheduledTaskLogItem::latest()->first();
$duration = $log->finished_at->diffInSeconds($log->started_at);
Oh Dear Ping Failures:
PingOhDearJob failures.OH_DEAR_DEBUG_LOGGING=true
OH_DEAR_QUEUE) to avoid contention.Multitenancy Conflicts:
PingOhDearJob fails with "tenant not set" errors.config/multitenancy.php:
'not_tenant_aware_jobs' => [
\Spatie\ScheduleMonitor\Jobs\PingOhDearJob::class,
],
Log Item Cleanup:
php artisan model:prune --model=Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem
delete_log_items_older_than_days in config (default: 30).php artisan tinker
>>> \Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem::query()->latest()->take(5)->get();
php artisan schedule-monitor:list --with-oh-dear-status
monitorName() is used for ambiguous tasks (e.g., closures):
$schedule->call(fn () => $this->processData())->daily()
->monitorName('process-data-job');
Custom Log Fields:
Extend MonitoredScheduledTaskLogItem to add metadata:
// app/Models/MonitoredScheduledTaskLogItem.php
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem as BaseLogItem;
class MonitoredScheduledTaskLogItem extends BaseLogItem {
protected $casts = [
'meta' => 'array',
];
public function setCustomField($key, $value) {
$this->meta[$key] = $value;
}
}
Use in schedule:
$schedule->command('backup:run')->daily()
->monitorName('backup')
->storeOutputInDb()
->then(function () {
$log = MonitoredScheduledTaskLogItem::latest()->first();
$log->setCustomField('backup_size', '1.2GB');
$log->save();
});
Custom Sync Logic: Override the sync command to filter tasks:
// app/Console/Commands/SyncSchedule.php
use Spatie\ScheduleMonitor\Commands\SyncSchedule as BaseSync;
class SyncSchedule extends BaseSync {
protected function getTasksToMonitor() {
return parent::getTasksToMonitor()->reject(fn ($task) =>
str_contains($task->command, 'test:')
);
}
}
Register in AppServiceProvider:
public function boot() {
$this->app->bind(
\Spatie\ScheduleMonitor\Commands\SyncSchedule::class,
app(\App\Console\Commands\SyncSchedule::class)
);
}
Webhook Alerts: Trigger custom alerts on task failures by listening to log events:
// app/Providers/EventServiceProvider.php
use Spatie\ScheduleMonitor\Events\ScheduledTaskFailed;
protected $listen = [
ScheduledTaskFailed::class => [
\App\Listeners\SendSlackAlert::class,
],
];
How can I help you explore Laravel packages today?